1use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17#[proc_macro_derive(Model, attributes(rustango))]
19pub fn derive_model(input: TokenStream) -> TokenStream {
20 let input = parse_macro_input!(input as DeriveInput);
21 expand(&input)
22 .unwrap_or_else(syn::Error::into_compile_error)
23 .into()
24}
25
26#[proc_macro_derive(ViewSet, attributes(viewset))]
59pub fn derive_viewset(input: TokenStream) -> TokenStream {
60 let input = parse_macro_input!(input as DeriveInput);
61 expand_viewset(&input)
62 .unwrap_or_else(syn::Error::into_compile_error)
63 .into()
64}
65
66#[proc_macro_derive(Form, attributes(form))]
94pub fn derive_form(input: TokenStream) -> TokenStream {
95 let input = parse_macro_input!(input as DeriveInput);
96 expand_form(&input)
97 .unwrap_or_else(syn::Error::into_compile_error)
98 .into()
99}
100
101#[proc_macro_derive(Serializer, attributes(serializer))]
114pub fn derive_serializer(input: TokenStream) -> TokenStream {
115 let input = parse_macro_input!(input as DeriveInput);
116 expand_serializer(&input)
117 .unwrap_or_else(syn::Error::into_compile_error)
118 .into()
119}
120
121#[proc_macro]
156pub fn embed_migrations(input: TokenStream) -> TokenStream {
157 expand_embed_migrations(input.into())
158 .unwrap_or_else(syn::Error::into_compile_error)
159 .into()
160}
161
162#[proc_macro_attribute]
185pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
186 expand_main(args.into(), item.into())
187 .unwrap_or_else(syn::Error::into_compile_error)
188 .into()
189}
190
191fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
192 let mut input: syn::ItemFn = syn::parse2(item)?;
193 if input.sig.asyncness.is_none() {
194 return Err(syn::Error::new(
195 input.sig.ident.span(),
196 "`#[rustango::main]` must wrap an `async fn`",
197 ));
198 }
199
200 let tokio_attr = if args.is_empty() {
203 quote! { #[::tokio::main] }
204 } else {
205 quote! { #[::tokio::main(#args)] }
206 };
207
208 let body = input.block.clone();
210 input.block = syn::parse2(quote! {{
211 {
212 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
213 let _ = tracing_subscriber::fmt()
216 .with_env_filter(
217 EnvFilter::try_from_default_env()
218 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
219 )
220 .try_init();
221 }
222 #body
223 }})?;
224
225 Ok(quote! {
226 #tokio_attr
227 #input
228 })
229}
230
231fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
232 let path_str = if input.is_empty() {
234 "./migrations".to_string()
235 } else {
236 let lit: LitStr = syn::parse2(input)?;
237 lit.value()
238 };
239
240 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
241 syn::Error::new(
242 proc_macro2::Span::call_site(),
243 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
244 )
245 })?;
246 let abs = std::path::Path::new(&manifest).join(&path_str);
247
248 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
249 if abs.is_dir() {
250 let read = std::fs::read_dir(&abs).map_err(|e| {
251 syn::Error::new(
252 proc_macro2::Span::call_site(),
253 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
254 )
255 })?;
256 for entry in read.flatten() {
257 let path = entry.path();
258 if !path.is_file() {
259 continue;
260 }
261 if path.extension().and_then(|s| s.to_str()) != Some("json") {
262 continue;
263 }
264 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
265 continue;
266 };
267 entries.push((stem.to_owned(), path));
268 }
269 }
270 entries.sort_by(|a, b| a.0.cmp(&b.0));
271
272 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
285 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
286 for (stem, path) in &entries {
287 let raw = std::fs::read_to_string(path).map_err(|e| {
288 syn::Error::new(
289 proc_macro2::Span::call_site(),
290 format!(
291 "embed_migrations!: cannot read {} for chain validation: {e}",
292 path.display()
293 ),
294 )
295 })?;
296 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
297 syn::Error::new(
298 proc_macro2::Span::call_site(),
299 format!(
300 "embed_migrations!: {} is not valid JSON: {e}",
301 path.display()
302 ),
303 )
304 })?;
305 let name = json
306 .get("name")
307 .and_then(|v| v.as_str())
308 .ok_or_else(|| {
309 syn::Error::new(
310 proc_macro2::Span::call_site(),
311 format!(
312 "embed_migrations!: {} is missing the `name` field",
313 path.display()
314 ),
315 )
316 })?
317 .to_owned();
318 if name != *stem {
319 return Err(syn::Error::new(
320 proc_macro2::Span::call_site(),
321 format!(
322 "embed_migrations!: file stem `{stem}` does not match the migration's \
323 `name` field `{name}` — rename the file or fix the JSON",
324 ),
325 ));
326 }
327 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
328 chain_names.push(name.clone());
329 prev_refs.push((name, prev));
330 }
331
332 let name_set: std::collections::HashSet<&str> =
333 chain_names.iter().map(String::as_str).collect();
334 for (name, prev) in &prev_refs {
335 if let Some(p) = prev {
336 if !name_set.contains(p.as_str()) {
337 return Err(syn::Error::new(
338 proc_macro2::Span::call_site(),
339 format!(
340 "embed_migrations!: broken migration chain — `{name}` declares \
341 prev=`{p}` but no migration with that name exists in {}",
342 abs.display()
343 ),
344 ));
345 }
346 }
347 }
348
349 let pairs: Vec<TokenStream2> = entries
350 .iter()
351 .map(|(name, path)| {
352 let path_lit = path.display().to_string();
353 quote! { (#name, ::core::include_str!(#path_lit)) }
354 })
355 .collect();
356
357 Ok(quote! {
358 {
359 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
360 __RUSTANGO_EMBEDDED
361 }
362 })
363}
364
365fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
366 let struct_name = &input.ident;
367
368 let Data::Struct(data) = &input.data else {
369 return Err(syn::Error::new_spanned(
370 struct_name,
371 "Model can only be derived on structs",
372 ));
373 };
374 let Fields::Named(named) = &data.fields else {
375 return Err(syn::Error::new_spanned(
376 struct_name,
377 "Model requires a struct with named fields",
378 ));
379 };
380
381 let container = parse_container_attrs(input)?;
382 let table = container
383 .table
384 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
385 let model_name = struct_name.to_string();
386
387 let collected = collect_fields(named, &table)?;
388
389 if let Some((ref display, span)) = container.display {
391 if !collected.field_names.iter().any(|n| n == display) {
392 return Err(syn::Error::new(
393 span,
394 format!("`display = \"{display}\"` does not match any field on this struct"),
395 ));
396 }
397 }
398 let display = container.display.map(|(name, _)| name);
399 let app_label = container.app.clone();
400
401 if let Some(admin) = &container.admin {
403 for (label, list) in [
404 ("list_display", &admin.list_display),
405 ("search_fields", &admin.search_fields),
406 ("readonly_fields", &admin.readonly_fields),
407 ("list_filter", &admin.list_filter),
408 ] {
409 if let Some((names, span)) = list {
410 for name in names {
411 if !collected.field_names.iter().any(|n| n == name) {
412 return Err(syn::Error::new(
413 *span,
414 format!(
415 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
416 ),
417 ));
418 }
419 }
420 }
421 }
422 if let Some((pairs, span)) = &admin.ordering {
423 for (name, _) in pairs {
424 if !collected.field_names.iter().any(|n| n == name) {
425 return Err(syn::Error::new(
426 *span,
427 format!(
428 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
429 ),
430 ));
431 }
432 }
433 }
434 if let Some((groups, span)) = &admin.fieldsets {
435 for (_, fields) in groups {
436 for name in fields {
437 if !collected.field_names.iter().any(|n| n == name) {
438 return Err(syn::Error::new(
439 *span,
440 format!(
441 "`fieldsets`: \"{name}\" is not a declared field on this struct"
442 ),
443 ));
444 }
445 }
446 }
447 }
448 }
449 if let Some(audit) = &container.audit {
450 if let Some((names, span)) = &audit.track {
451 for name in names {
452 if !collected.field_names.iter().any(|n| n == name) {
453 return Err(syn::Error::new(
454 *span,
455 format!(
456 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
457 ),
458 ));
459 }
460 }
461 }
462 }
463
464 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
467 audit
468 .track
469 .as_ref()
470 .map(|(names, _)| names.clone())
471 .unwrap_or_default()
472 });
473
474 let mut all_indexes: Vec<IndexAttr> = container.indexes;
476 for field in &named.named {
477 let ident = field.ident.as_ref().expect("named");
478 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
481 if fa.index {
482 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
483 let auto_name = if fa.index_unique {
484 format!("{table}_{col_name}_uq_idx")
485 } else {
486 format!("{table}_{col_name}_idx")
487 };
488 all_indexes.push(IndexAttr {
489 name: fa.index_name.or(Some(auto_name)),
490 columns: vec![col_name],
491 unique: fa.index_unique,
492 });
493 }
494 }
495 }
496
497 let model_impl = model_impl_tokens(
498 struct_name,
499 &model_name,
500 &table,
501 display.as_deref(),
502 app_label.as_deref(),
503 container.admin.as_ref(),
504 &collected.field_schemas,
505 collected.soft_delete_column.as_deref(),
506 container.permissions,
507 audit_track_names.as_deref(),
508 &container.m2m,
509 &all_indexes,
510 &container.checks,
511 &container.composite_fks,
512 &container.generic_fks,
513 container.scope.as_deref(),
514 );
515 let module_ident = column_module_ident(struct_name);
516 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
517 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
518 let track_set: Option<std::collections::HashSet<&str>> = audit
519 .track
520 .as_ref()
521 .map(|(names, _)| names.iter().map(String::as_str).collect());
522 collected
523 .column_entries
524 .iter()
525 .filter(|c| {
526 track_set
527 .as_ref()
528 .map_or(true, |s| s.contains(c.name.as_str()))
529 })
530 .collect()
531 });
532 let inherent_impl = inherent_impl_tokens(
533 struct_name,
534 &collected,
535 collected.primary_key.as_ref(),
536 &column_consts,
537 audited_fields.as_deref(),
538 &all_indexes,
539 );
540 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
541 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
542 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
543 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
544
545 Ok(quote! {
546 #model_impl
547 #inherent_impl
548 #from_row_impl
549 #column_module
550 #reverse_helpers
551 #m2m_accessors
552
553 ::rustango::core::inventory::submit! {
554 ::rustango::core::ModelEntry {
555 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
556 module_path: ::core::module_path!(),
561 }
562 }
563 })
564}
565
566fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
577 let arms = fk_relations.iter().map(|rel| {
578 let parent_ty = &rel.parent_type;
579 let fk_col = rel.fk_column.as_str();
580 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
583 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
584 let assign = if rel.nullable {
585 quote! {
586 self.#field_ident = ::core::option::Option::Some(
587 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
588 );
589 }
590 } else {
591 quote! {
592 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
593 }
594 };
595 quote! {
596 #fk_col => {
597 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
598 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
605 ::rustango::core::SqlValue::#variant_ident(v) => v,
606 _other => {
607 ::core::debug_assert!(
608 false,
609 "rustango macro bug: load_related on FK `{}` expected \
610 SqlValue::{} from parent's __rustango_pk_value but got \
611 {:?} — file a bug at https://github.com/ujeenet/rustango",
612 #fk_col,
613 ::core::stringify!(#variant_ident),
614 _other,
615 );
616 #default_expr
617 }
618 };
619 #assign
620 ::core::result::Result::Ok(true)
621 }
622 }
623 });
624 quote! {
625 impl ::rustango::sql::LoadRelated for #struct_name {
626 #[allow(unused_variables)]
627 fn __rustango_load_related(
628 &mut self,
629 row: &::rustango::sql::sqlx::postgres::PgRow,
630 field_name: &str,
631 alias: &str,
632 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
633 match field_name {
634 #( #arms )*
635 _ => ::core::result::Result::Ok(false),
636 }
637 }
638 }
639 }
640}
641
642fn load_related_impl_my_tokens(
650 struct_name: &syn::Ident,
651 fk_relations: &[FkRelation],
652) -> TokenStream2 {
653 let arms = fk_relations.iter().map(|rel| {
654 let parent_ty = &rel.parent_type;
655 let fk_col = rel.fk_column.as_str();
656 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
657 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
658 let assign = if rel.nullable {
659 quote! {
660 __self.#field_ident = ::core::option::Option::Some(
661 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
662 );
663 }
664 } else {
665 quote! {
666 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
667 }
668 };
669 quote! {
674 #fk_col => {
675 let _parent: #parent_ty =
676 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
677 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
680 ::rustango::core::SqlValue::#variant_ident(v) => v,
681 _other => {
682 ::core::debug_assert!(
683 false,
684 "rustango macro bug: load_related on FK `{}` expected \
685 SqlValue::{} from parent's __rustango_pk_value but got \
686 {:?} — file a bug at https://github.com/ujeenet/rustango",
687 #fk_col,
688 ::core::stringify!(#variant_ident),
689 _other,
690 );
691 #default_expr
692 }
693 };
694 #assign
695 ::core::result::Result::Ok(true)
696 }
697 }
698 });
699 quote! {
700 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
701 #( #arms )*
702 });
703 }
704}
705
706fn load_related_impl_sqlite_tokens(
710 struct_name: &syn::Ident,
711 fk_relations: &[FkRelation],
712) -> TokenStream2 {
713 let arms = fk_relations.iter().map(|rel| {
714 let parent_ty = &rel.parent_type;
715 let fk_col = rel.fk_column.as_str();
716 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
717 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
718 let assign = if rel.nullable {
719 quote! {
720 __self.#field_ident = ::core::option::Option::Some(
721 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
722 );
723 }
724 } else {
725 quote! {
726 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
727 }
728 };
729 quote! {
730 #fk_col => {
731 let _parent: #parent_ty =
732 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
733 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
734 ::rustango::core::SqlValue::#variant_ident(v) => v,
735 _other => {
736 ::core::debug_assert!(
737 false,
738 "rustango macro bug: load_related on FK `{}` expected \
739 SqlValue::{} from parent's __rustango_pk_value but got \
740 {:?} — file a bug at https://github.com/ujeenet/rustango",
741 #fk_col,
742 ::core::stringify!(#variant_ident),
743 _other,
744 );
745 #default_expr
746 }
747 };
748 #assign
749 ::core::result::Result::Ok(true)
750 }
751 }
752 });
753 quote! {
754 ::rustango::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias| {
755 #( #arms )*
756 });
757 }
758}
759
760fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
768 let arms = fk_relations.iter().map(|rel| {
769 let fk_col = rel.fk_column.as_str();
770 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
771 if rel.pk_kind == DetectedKind::I64 {
772 if rel.nullable {
778 quote! {
779 #fk_col => self.#field_ident
780 .as_ref()
781 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
782 }
783 } else {
784 quote! {
785 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
786 }
787 }
788 } else {
789 quote! {
797 #fk_col => ::core::option::Option::None,
798 }
799 }
800 });
801 let value_arms = fk_relations.iter().map(|rel| {
807 let fk_col = rel.fk_column.as_str();
808 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
809 if rel.nullable {
810 quote! {
811 #fk_col => self.#field_ident
812 .as_ref()
813 .map(|fk| ::core::convert::Into::<::rustango::core::SqlValue>::into(
814 ::rustango::sql::ForeignKey::pk(fk)
815 )),
816 }
817 } else {
818 quote! {
819 #fk_col => ::core::option::Option::Some(
820 ::core::convert::Into::<::rustango::core::SqlValue>::into(
821 self.#field_ident.pk()
822 )
823 ),
824 }
825 }
826 });
827 quote! {
828 impl ::rustango::sql::FkPkAccess for #struct_name {
829 #[allow(unused_variables)]
830 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
831 match field_name {
832 #( #arms )*
833 _ => ::core::option::Option::None,
834 }
835 }
836 #[allow(unused_variables)]
837 fn __rustango_fk_pk_value(
838 &self,
839 field_name: &str,
840 ) -> ::core::option::Option<::rustango::core::SqlValue> {
841 match field_name {
842 #( #value_arms )*
843 _ => ::core::option::Option::None,
844 }
845 }
846 }
847 }
848}
849
850fn reverse_helper_tokens(child_ident: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
856 if fk_relations.is_empty() {
857 return TokenStream2::new();
858 }
859 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
863 let method_ident = syn::Ident::new(&suffix, child_ident.span());
864 let impls = fk_relations.iter().map(|rel| {
865 let parent_ty = &rel.parent_type;
866 let fk_col = rel.fk_column.as_str();
867 let doc = format!(
868 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
869 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
870 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
871 further `{child_ident}::objects()` filters via direct queryset use."
872 );
873 quote! {
874 impl #parent_ty {
875 #[doc = #doc]
876 pub async fn #method_ident<'_c, _E>(
881 &self,
882 _executor: _E,
883 ) -> ::core::result::Result<
884 ::std::vec::Vec<#child_ident>,
885 ::rustango::sql::ExecError,
886 >
887 where
888 _E: ::rustango::sql::sqlx::Executor<
889 '_c,
890 Database = ::rustango::sql::sqlx::Postgres,
891 >,
892 {
893 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
894 ::rustango::query::QuerySet::<#child_ident>::new()
895 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
896 .fetch_on(_executor)
897 .await
898 }
899 }
900 }
901 });
902 quote! { #( #impls )* }
903}
904
905fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
908 if m2m_relations.is_empty() {
909 return TokenStream2::new();
910 }
911 let methods = m2m_relations.iter().map(|rel| {
912 let method_name = format!("{}_m2m", rel.name);
913 let method_ident = syn::Ident::new(&method_name, struct_name.span());
914 let through = rel.through.as_str();
915 let src_col = rel.src.as_str();
916 let dst_col = rel.dst.as_str();
917 quote! {
918 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
919 ::rustango::sql::M2MManager {
920 src_pk: self.__rustango_pk_value(),
921 through: #through,
922 src_col: #src_col,
923 dst_col: #dst_col,
924 }
925 }
926 }
927 });
928 quote! {
929 impl #struct_name {
930 #( #methods )*
931 }
932 }
933}
934
935struct ColumnEntry {
936 ident: syn::Ident,
939 value_ty: Type,
941 name: String,
943 column: String,
945 field_type_tokens: TokenStream2,
947}
948
949struct CollectedFields {
950 field_schemas: Vec<TokenStream2>,
951 from_row_inits: Vec<TokenStream2>,
952 from_aliased_row_inits: Vec<TokenStream2>,
956 insert_columns: Vec<TokenStream2>,
959 insert_values: Vec<TokenStream2>,
962 insert_pushes: Vec<TokenStream2>,
967 returning_cols: Vec<TokenStream2>,
970 auto_assigns: Vec<TokenStream2>,
973 auto_field_idents: Vec<(syn::Ident, String)>,
977 first_auto_value_ty: Option<Type>,
980 bulk_pushes_no_auto: Vec<TokenStream2>,
984 bulk_pushes_all: Vec<TokenStream2>,
988 bulk_columns_no_auto: Vec<TokenStream2>,
991 bulk_columns_all: Vec<TokenStream2>,
994 bulk_auto_uniformity: Vec<TokenStream2>,
998 first_auto_ident: Option<syn::Ident>,
1001 has_auto: bool,
1003 pk_is_auto: bool,
1007 update_assignments: Vec<TokenStream2>,
1010 upsert_update_columns: Vec<TokenStream2>,
1013 primary_key: Option<(syn::Ident, String)>,
1014 column_entries: Vec<ColumnEntry>,
1015 field_names: Vec<String>,
1018 fk_relations: Vec<FkRelation>,
1023 soft_delete_column: Option<String>,
1028}
1029
1030#[derive(Clone)]
1031struct FkRelation {
1032 parent_type: Type,
1035 fk_column: String,
1038 pk_kind: DetectedKind,
1043 nullable: bool,
1048}
1049
1050fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
1051 let cap = named.named.len();
1052 let mut out = CollectedFields {
1053 field_schemas: Vec::with_capacity(cap),
1054 from_row_inits: Vec::with_capacity(cap),
1055 from_aliased_row_inits: Vec::with_capacity(cap),
1056 insert_columns: Vec::with_capacity(cap),
1057 insert_values: Vec::with_capacity(cap),
1058 insert_pushes: Vec::with_capacity(cap),
1059 returning_cols: Vec::new(),
1060 auto_assigns: Vec::new(),
1061 auto_field_idents: Vec::new(),
1062 first_auto_value_ty: None,
1063 bulk_pushes_no_auto: Vec::with_capacity(cap),
1064 bulk_pushes_all: Vec::with_capacity(cap),
1065 bulk_columns_no_auto: Vec::with_capacity(cap),
1066 bulk_columns_all: Vec::with_capacity(cap),
1067 bulk_auto_uniformity: Vec::new(),
1068 first_auto_ident: None,
1069 has_auto: false,
1070 pk_is_auto: false,
1071 update_assignments: Vec::with_capacity(cap),
1072 upsert_update_columns: Vec::with_capacity(cap),
1073 primary_key: None,
1074 column_entries: Vec::with_capacity(cap),
1075 field_names: Vec::with_capacity(cap),
1076 fk_relations: Vec::new(),
1077 soft_delete_column: None,
1078 };
1079
1080 for field in &named.named {
1081 let info = process_field(field, table)?;
1082 out.field_names.push(info.ident.to_string());
1083 out.field_schemas.push(info.schema);
1084 out.from_row_inits.push(info.from_row_init);
1085 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1086 if let Some(parent_ty) = info.fk_inner.clone() {
1087 out.fk_relations.push(FkRelation {
1088 parent_type: parent_ty,
1089 fk_column: info.column.clone(),
1090 pk_kind: info.fk_pk_kind,
1091 nullable: info.nullable,
1092 });
1093 }
1094 if info.soft_delete {
1095 if out.soft_delete_column.is_some() {
1096 return Err(syn::Error::new_spanned(
1097 field,
1098 "only one field may be marked `#[rustango(soft_delete)]`",
1099 ));
1100 }
1101 out.soft_delete_column = Some(info.column.clone());
1102 }
1103 let column = info.column.as_str();
1104 let ident = info.ident;
1105 if info.generated_as.is_some() {
1114 out.column_entries.push(ColumnEntry {
1115 ident: ident.clone(),
1116 value_ty: info.value_ty.clone(),
1117 name: ident.to_string(),
1118 column: info.column.clone(),
1119 field_type_tokens: info.field_type_tokens,
1120 });
1121 continue;
1122 }
1123 out.insert_columns.push(quote!(#column));
1124 out.insert_values.push(quote! {
1125 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1126 ::core::clone::Clone::clone(&self.#ident)
1127 )
1128 });
1129 if info.auto {
1130 out.has_auto = true;
1131 if out.first_auto_ident.is_none() {
1132 out.first_auto_ident = Some(ident.clone());
1133 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1134 }
1135 out.returning_cols.push(quote!(#column));
1136 out.auto_field_idents
1137 .push((ident.clone(), info.column.clone()));
1138 out.auto_assigns.push(quote! {
1139 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1140 });
1141 out.insert_pushes.push(quote! {
1142 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1143 _columns.push(#column);
1144 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1145 ::core::clone::Clone::clone(_v)
1146 ));
1147 }
1148 });
1149 out.bulk_columns_all.push(quote!(#column));
1152 out.bulk_pushes_all.push(quote! {
1153 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1154 ::core::clone::Clone::clone(&_row.#ident)
1155 ));
1156 });
1157 let ident_clone = ident.clone();
1161 out.bulk_auto_uniformity.push(quote! {
1162 for _r in rows.iter().skip(1) {
1163 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1164 return ::core::result::Result::Err(
1165 ::rustango::sql::ExecError::Sql(
1166 ::rustango::sql::SqlError::BulkAutoMixed
1167 )
1168 );
1169 }
1170 }
1171 });
1172 } else {
1173 out.insert_pushes.push(quote! {
1174 _columns.push(#column);
1175 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1176 ::core::clone::Clone::clone(&self.#ident)
1177 ));
1178 });
1179 out.bulk_columns_no_auto.push(quote!(#column));
1181 out.bulk_columns_all.push(quote!(#column));
1182 let push_expr = quote! {
1183 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1184 ::core::clone::Clone::clone(&_row.#ident)
1185 ));
1186 };
1187 out.bulk_pushes_no_auto.push(push_expr.clone());
1188 out.bulk_pushes_all.push(push_expr);
1189 }
1190 if info.primary_key {
1191 if out.primary_key.is_some() {
1192 return Err(syn::Error::new_spanned(
1193 field,
1194 "only one field may be marked `#[rustango(primary_key)]`",
1195 ));
1196 }
1197 out.primary_key = Some((ident.clone(), info.column.clone()));
1198 if info.auto {
1199 out.pk_is_auto = true;
1200 }
1201 } else if info.auto_now_add {
1202 } else if info.auto_now {
1204 out.update_assignments.push(quote! {
1209 ::rustango::core::Assignment {
1210 column: #column,
1211 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1212 ::chrono::Utc::now()
1213 ),
1214 }
1215 });
1216 out.upsert_update_columns.push(quote!(#column));
1217 } else {
1218 out.update_assignments.push(quote! {
1219 ::rustango::core::Assignment {
1220 column: #column,
1221 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1222 ::core::clone::Clone::clone(&self.#ident)
1223 ),
1224 }
1225 });
1226 out.upsert_update_columns.push(quote!(#column));
1227 }
1228 out.column_entries.push(ColumnEntry {
1229 ident: ident.clone(),
1230 value_ty: info.value_ty.clone(),
1231 name: ident.to_string(),
1232 column: info.column.clone(),
1233 field_type_tokens: info.field_type_tokens,
1234 });
1235 }
1236 Ok(out)
1237}
1238
1239fn model_impl_tokens(
1240 struct_name: &syn::Ident,
1241 model_name: &str,
1242 table: &str,
1243 display: Option<&str>,
1244 app_label: Option<&str>,
1245 admin: Option<&AdminAttrs>,
1246 field_schemas: &[TokenStream2],
1247 soft_delete_column: Option<&str>,
1248 permissions: bool,
1249 audit_track: Option<&[String]>,
1250 m2m_relations: &[M2MAttr],
1251 indexes: &[IndexAttr],
1252 checks: &[CheckAttr],
1253 composite_fks: &[CompositeFkAttr],
1254 generic_fks: &[GenericFkAttr],
1255 scope: Option<&str>,
1256) -> TokenStream2 {
1257 let display_tokens = if let Some(name) = display {
1258 quote!(::core::option::Option::Some(#name))
1259 } else {
1260 quote!(::core::option::Option::None)
1261 };
1262 let app_label_tokens = if let Some(name) = app_label {
1263 quote!(::core::option::Option::Some(#name))
1264 } else {
1265 quote!(::core::option::Option::None)
1266 };
1267 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1268 quote!(::core::option::Option::Some(#col))
1269 } else {
1270 quote!(::core::option::Option::None)
1271 };
1272 let audit_track_tokens = match audit_track {
1273 None => quote!(::core::option::Option::None),
1274 Some(names) => {
1275 let lits = names.iter().map(|n| n.as_str());
1276 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1277 }
1278 };
1279 let admin_tokens = admin_config_tokens(admin);
1280 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1284 Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1285 _ => quote!(::rustango::core::ModelScope::Tenant),
1286 };
1287 let indexes_tokens = indexes.iter().map(|idx| {
1288 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1289 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1290 let unique = idx.unique;
1291 quote! {
1292 ::rustango::core::IndexSchema {
1293 name: #name,
1294 columns: &[ #(#cols),* ],
1295 unique: #unique,
1296 }
1297 }
1298 });
1299 let checks_tokens = checks.iter().map(|c| {
1300 let name = c.name.as_str();
1301 let expr = c.expr.as_str();
1302 quote! {
1303 ::rustango::core::CheckConstraint {
1304 name: #name,
1305 expr: #expr,
1306 }
1307 }
1308 });
1309 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1310 let name = rel.name.as_str();
1311 let to = rel.to.as_str();
1312 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1313 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1314 quote! {
1315 ::rustango::core::CompositeFkRelation {
1316 name: #name,
1317 to: #to,
1318 from: &[ #(#from_cols),* ],
1319 on: &[ #(#on_cols),* ],
1320 }
1321 }
1322 });
1323 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1324 let name = rel.name.as_str();
1325 let ct_col = rel.ct_column.as_str();
1326 let pk_col = rel.pk_column.as_str();
1327 quote! {
1328 ::rustango::core::GenericRelation {
1329 name: #name,
1330 ct_column: #ct_col,
1331 pk_column: #pk_col,
1332 }
1333 }
1334 });
1335 let m2m_tokens = m2m_relations.iter().map(|rel| {
1336 let name = rel.name.as_str();
1337 let to = rel.to.as_str();
1338 let through = rel.through.as_str();
1339 let src = rel.src.as_str();
1340 let dst = rel.dst.as_str();
1341 quote! {
1342 ::rustango::core::M2MRelation {
1343 name: #name,
1344 to: #to,
1345 through: #through,
1346 src_col: #src,
1347 dst_col: #dst,
1348 }
1349 }
1350 });
1351 quote! {
1352 impl ::rustango::core::Model for #struct_name {
1353 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1354 name: #model_name,
1355 table: #table,
1356 fields: &[ #(#field_schemas),* ],
1357 display: #display_tokens,
1358 app_label: #app_label_tokens,
1359 admin: #admin_tokens,
1360 soft_delete_column: #soft_delete_tokens,
1361 permissions: #permissions,
1362 audit_track: #audit_track_tokens,
1363 m2m: &[ #(#m2m_tokens),* ],
1364 indexes: &[ #(#indexes_tokens),* ],
1365 check_constraints: &[ #(#checks_tokens),* ],
1366 composite_relations: &[ #(#composite_fk_tokens),* ],
1367 generic_relations: &[ #(#generic_fk_tokens),* ],
1368 scope: #scope_tokens,
1369 };
1370 }
1371 }
1372}
1373
1374fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1378 let Some(admin) = admin else {
1379 return quote!(::core::option::Option::None);
1380 };
1381
1382 let list_display = admin
1383 .list_display
1384 .as_ref()
1385 .map(|(v, _)| v.as_slice())
1386 .unwrap_or(&[]);
1387 let list_display_lits = list_display.iter().map(|s| s.as_str());
1388
1389 let search_fields = admin
1390 .search_fields
1391 .as_ref()
1392 .map(|(v, _)| v.as_slice())
1393 .unwrap_or(&[]);
1394 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1395
1396 let readonly_fields = admin
1397 .readonly_fields
1398 .as_ref()
1399 .map(|(v, _)| v.as_slice())
1400 .unwrap_or(&[]);
1401 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1402
1403 let list_filter = admin
1404 .list_filter
1405 .as_ref()
1406 .map(|(v, _)| v.as_slice())
1407 .unwrap_or(&[]);
1408 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1409
1410 let actions = admin
1411 .actions
1412 .as_ref()
1413 .map(|(v, _)| v.as_slice())
1414 .unwrap_or(&[]);
1415 let actions_lits = actions.iter().map(|s| s.as_str());
1416
1417 let fieldsets = admin
1418 .fieldsets
1419 .as_ref()
1420 .map(|(v, _)| v.as_slice())
1421 .unwrap_or(&[]);
1422 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1423 let title = title.as_str();
1424 let field_lits = fields.iter().map(|s| s.as_str());
1425 quote!(::rustango::core::Fieldset {
1426 title: #title,
1427 fields: &[ #( #field_lits ),* ],
1428 })
1429 });
1430
1431 let list_per_page = admin.list_per_page.unwrap_or(0);
1432
1433 let ordering_pairs = admin
1434 .ordering
1435 .as_ref()
1436 .map(|(v, _)| v.as_slice())
1437 .unwrap_or(&[]);
1438 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1439 let name = name.as_str();
1440 let desc = *desc;
1441 quote!((#name, #desc))
1442 });
1443
1444 quote! {
1445 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1446 list_display: &[ #( #list_display_lits ),* ],
1447 search_fields: &[ #( #search_fields_lits ),* ],
1448 list_per_page: #list_per_page,
1449 ordering: &[ #( #ordering_tokens ),* ],
1450 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1451 list_filter: &[ #( #list_filter_lits ),* ],
1452 actions: &[ #( #actions_lits ),* ],
1453 fieldsets: &[ #( #fieldset_tokens ),* ],
1454 })
1455 }
1456}
1457
1458fn inherent_impl_tokens(
1459 struct_name: &syn::Ident,
1460 fields: &CollectedFields,
1461 primary_key: Option<&(syn::Ident, String)>,
1462 column_consts: &TokenStream2,
1463 audited_fields: Option<&[&ColumnEntry]>,
1464 indexes: &[IndexAttr],
1465) -> TokenStream2 {
1466 let executor_passes_to_data_write = if audited_fields.is_some() {
1472 quote!(&mut *_executor)
1473 } else {
1474 quote!(_executor)
1475 };
1476 let executor_param = if audited_fields.is_some() {
1477 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1478 } else {
1479 quote!(_executor: _E)
1480 };
1481 let executor_generics = if audited_fields.is_some() {
1482 quote!()
1483 } else {
1484 quote!(<'_c, _E>)
1485 };
1486 let executor_where = if audited_fields.is_some() {
1487 quote!()
1488 } else {
1489 quote! {
1490 where
1491 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1492 }
1493 };
1494 let pool_to_save_on = if audited_fields.is_some() {
1499 quote! {
1500 let mut _conn = pool.acquire().await?;
1501 self.save_on(&mut *_conn).await
1502 }
1503 } else {
1504 quote!(self.save_on(pool).await)
1505 };
1506 let pool_to_insert_on = if audited_fields.is_some() {
1507 quote! {
1508 let mut _conn = pool.acquire().await?;
1509 self.insert_on(&mut *_conn).await
1510 }
1511 } else {
1512 quote!(self.insert_on(pool).await)
1513 };
1514 let pool_to_delete_on = if audited_fields.is_some() {
1515 quote! {
1516 let mut _conn = pool.acquire().await?;
1517 self.delete_on(&mut *_conn).await
1518 }
1519 } else {
1520 quote!(self.delete_on(pool).await)
1521 };
1522 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1523 quote! {
1524 let mut _conn = pool.acquire().await?;
1525 Self::bulk_insert_on(rows, &mut *_conn).await
1526 }
1527 } else {
1528 quote!(Self::bulk_insert_on(rows, pool).await)
1529 };
1530 let pool_to_upsert_on = if audited_fields.is_some() {
1537 quote! {
1538 let mut _conn = pool.acquire().await?;
1539 self.upsert_on(&mut *_conn).await
1540 }
1541 } else {
1542 quote!(self.upsert_on(pool).await)
1543 };
1544
1545 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1563 quote!()
1572 } else if audited_fields.is_some() && fields.has_auto {
1573 quote!()
1576 } else if fields.has_auto {
1577 let pushes = &fields.insert_pushes;
1578 let returning_cols = &fields.returning_cols;
1579 quote! {
1580 pub async fn insert_pool(
1586 &mut self,
1587 pool: &::rustango::sql::Pool,
1588 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1589 let mut _columns: ::std::vec::Vec<&'static str> =
1590 ::std::vec::Vec::new();
1591 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1592 ::std::vec::Vec::new();
1593 #( #pushes )*
1594 let _query = ::rustango::core::InsertQuery {
1595 model: <Self as ::rustango::core::Model>::SCHEMA,
1596 columns: _columns,
1597 values: _values,
1598 returning: ::std::vec![ #( #returning_cols ),* ],
1599 on_conflict: ::core::option::Option::None,
1600 };
1601 let _result = ::rustango::sql::insert_returning_pool(
1602 pool, &_query,
1603 ).await?;
1604 ::rustango::sql::apply_auto_pk_pool(_result, self)
1605 }
1606 }
1607 } else {
1608 let insert_columns = &fields.insert_columns;
1609 let insert_values = &fields.insert_values;
1610 quote! {
1611 pub async fn insert_pool(
1618 &self,
1619 pool: &::rustango::sql::Pool,
1620 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1621 let _query = ::rustango::core::InsertQuery {
1622 model: <Self as ::rustango::core::Model>::SCHEMA,
1623 columns: ::std::vec![ #( #insert_columns ),* ],
1624 values: ::std::vec![ #( #insert_values ),* ],
1625 returning: ::std::vec::Vec::new(),
1626 on_conflict: ::core::option::Option::None,
1627 };
1628 ::rustango::sql::insert_pool(pool, &_query).await
1629 }
1630 }
1631 };
1632
1633 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1646 .map(|tracked| {
1647 tracked
1648 .iter()
1649 .map(|c| {
1650 let column_lit = c.column.as_str();
1651 let ident = &c.ident;
1652 quote! {
1653 (
1654 #column_lit,
1655 ::serde_json::to_value(&self.#ident)
1656 .unwrap_or(::serde_json::Value::Null),
1657 )
1658 }
1659 })
1660 .collect()
1661 })
1662 .unwrap_or_default();
1663 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1664 if fields.pk_is_auto {
1665 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1666 } else {
1667 quote!(::std::format!("{}", &self.#pk_ident))
1668 }
1669 } else {
1670 quote!(::std::string::String::new())
1671 };
1672 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1673 if audited_fields.is_some() {
1674 let pairs = audit_pair_tokens.iter();
1675 let pk_str = audit_pk_to_string.clone();
1676 quote! {
1677 let _audit_entry = ::rustango::audit::PendingEntry {
1678 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1679 entity_pk: #pk_str,
1680 operation: #op_path,
1681 source: ::rustango::audit::current_source(),
1682 changes: ::rustango::audit::snapshot_changes(&[
1683 #( #pairs ),*
1684 ]),
1685 };
1686 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1687 }
1688 } else {
1689 quote!()
1690 }
1691 };
1692 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1693 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1694 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1695 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1696
1697 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1713 let pk_column_lit = pk_col.as_str();
1714 let assignments = &fields.update_assignments;
1715 if audited_fields.is_some() {
1716 if fields.pk_is_auto {
1717 quote!()
1721 } else {
1722 let pairs = audit_pair_tokens.iter();
1723 let pk_str = audit_pk_to_string.clone();
1724 quote! {
1725 pub async fn save_pool(
1739 &mut self,
1740 pool: &::rustango::sql::Pool,
1741 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1742 let _query = ::rustango::core::UpdateQuery {
1743 model: <Self as ::rustango::core::Model>::SCHEMA,
1744 set: ::std::vec![ #( #assignments ),* ],
1745 where_clause: ::rustango::core::WhereExpr::Predicate(
1746 ::rustango::core::Filter {
1747 column: #pk_column_lit,
1748 op: ::rustango::core::Op::Eq,
1749 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1750 ::core::clone::Clone::clone(&self.#pk_ident)
1751 ),
1752 }
1753 ),
1754 };
1755 let _audit_entry = ::rustango::audit::PendingEntry {
1756 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1757 entity_pk: #pk_str,
1758 operation: ::rustango::audit::AuditOp::Update,
1759 source: ::rustango::audit::current_source(),
1760 changes: ::rustango::audit::snapshot_changes(&[
1761 #( #pairs ),*
1762 ]),
1763 };
1764 let _ = ::rustango::audit::save_one_with_audit_pool(
1765 pool, &_query, &_audit_entry,
1766 ).await?;
1767 ::core::result::Result::Ok(())
1768 }
1769 }
1770 }
1771 } else {
1772 let dispatch_unset = if fields.pk_is_auto {
1773 quote! {
1774 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1775 return self.insert_pool(pool).await;
1776 }
1777 }
1778 } else {
1779 quote!()
1780 };
1781 quote! {
1782 pub async fn save_pool(
1789 &mut self,
1790 pool: &::rustango::sql::Pool,
1791 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1792 #dispatch_unset
1793 let _query = ::rustango::core::UpdateQuery {
1794 model: <Self as ::rustango::core::Model>::SCHEMA,
1795 set: ::std::vec![ #( #assignments ),* ],
1796 where_clause: ::rustango::core::WhereExpr::Predicate(
1797 ::rustango::core::Filter {
1798 column: #pk_column_lit,
1799 op: ::rustango::core::Op::Eq,
1800 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1801 ::core::clone::Clone::clone(&self.#pk_ident)
1802 ),
1803 }
1804 ),
1805 };
1806 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1807 ::core::result::Result::Ok(())
1808 }
1809 }
1810 }
1811 } else {
1812 quote!()
1813 };
1814
1815 let pool_insert_method = if audited_fields.is_some() {
1822 if let Some(_) = primary_key {
1823 let pushes = if fields.has_auto {
1824 fields.insert_pushes.clone()
1825 } else {
1826 fields
1831 .insert_columns
1832 .iter()
1833 .zip(&fields.insert_values)
1834 .map(|(col, val)| {
1835 quote! {
1836 _columns.push(#col);
1837 _values.push(#val);
1838 }
1839 })
1840 .collect()
1841 };
1842 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1843 fields.returning_cols.clone()
1844 } else {
1845 primary_key
1852 .map(|(_, col)| {
1853 let lit = col.as_str();
1854 vec![quote!(#lit)]
1855 })
1856 .unwrap_or_default()
1857 };
1858 let pairs = audit_pair_tokens.iter();
1859 let pk_str = audit_pk_to_string.clone();
1860 quote! {
1861 pub async fn insert_pool(
1870 &mut self,
1871 pool: &::rustango::sql::Pool,
1872 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1873 let mut _columns: ::std::vec::Vec<&'static str> =
1874 ::std::vec::Vec::new();
1875 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1876 ::std::vec::Vec::new();
1877 #( #pushes )*
1878 let _query = ::rustango::core::InsertQuery {
1879 model: <Self as ::rustango::core::Model>::SCHEMA,
1880 columns: _columns,
1881 values: _values,
1882 returning: ::std::vec![ #( #returning_cols ),* ],
1883 on_conflict: ::core::option::Option::None,
1884 };
1885 let _audit_entry = ::rustango::audit::PendingEntry {
1886 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1887 entity_pk: #pk_str,
1888 operation: ::rustango::audit::AuditOp::Create,
1889 source: ::rustango::audit::current_source(),
1890 changes: ::rustango::audit::snapshot_changes(&[
1891 #( #pairs ),*
1892 ]),
1893 };
1894 let _result = ::rustango::audit::insert_one_with_audit_pool(
1895 pool, &_query, &_audit_entry,
1896 ).await?;
1897 ::rustango::sql::apply_auto_pk_pool(_result, self)
1898 }
1899 }
1900 } else {
1901 quote!()
1902 }
1903 } else {
1904 pool_insert_method
1906 };
1907
1908 let pool_save_method = if let Some(tracked) = audited_fields {
1929 if let Some((pk_ident, pk_col)) = primary_key {
1930 let pk_column_lit = pk_col.as_str();
1931 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1935 let pk_str = audit_pk_to_string.clone();
1936 let mk_before_pairs =
1941 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1942 tracked
1943 .iter()
1944 .map(|c| {
1945 let column_lit = c.column.as_str();
1946 let value_ty = &c.value_ty;
1947 quote! {
1948 (
1949 #column_lit,
1950 match #getter::<#value_ty>(
1951 _audit_before_row, #column_lit,
1952 ) {
1953 ::core::result::Result::Ok(v) => {
1954 ::serde_json::to_value(&v)
1955 .unwrap_or(::serde_json::Value::Null)
1956 }
1957 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1958 },
1959 )
1960 }
1961 })
1962 .collect()
1963 };
1964 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
1965 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
1966 let before_pairs_my: Vec<proc_macro2::TokenStream> =
1967 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
1968 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
1969 mk_before_pairs(quote!(::rustango::sql::try_get_returning_sqlite));
1970 let pg_select_cols: String = tracked
1971 .iter()
1972 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1973 .collect::<Vec<_>>()
1974 .join(", ");
1975 let my_select_cols: String = tracked
1976 .iter()
1977 .map(|c| format!("`{}`", c.column.replace('`', "``")))
1978 .collect::<Vec<_>>()
1979 .join(", ");
1980 let sqlite_select_cols: String = pg_select_cols.clone();
1984 let pk_value_for_bind = if fields.pk_is_auto {
1985 quote!(self.#pk_ident.get().copied().unwrap_or_default())
1986 } else {
1987 quote!(::core::clone::Clone::clone(&self.#pk_ident))
1988 };
1989 let assignments = &fields.update_assignments;
1990 let unset_dispatch = if fields.has_auto {
1991 quote! {
1992 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1993 return self.insert_pool(pool).await;
1994 }
1995 }
1996 } else {
1997 quote!()
1998 };
1999 quote! {
2000 pub async fn save_pool(
2014 &mut self,
2015 pool: &::rustango::sql::Pool,
2016 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2017 #unset_dispatch
2018 let _query = ::rustango::core::UpdateQuery {
2019 model: <Self as ::rustango::core::Model>::SCHEMA,
2020 set: ::std::vec![ #( #assignments ),* ],
2021 where_clause: ::rustango::core::WhereExpr::Predicate(
2022 ::rustango::core::Filter {
2023 column: #pk_column_lit,
2024 op: ::rustango::core::Op::Eq,
2025 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2026 ::core::clone::Clone::clone(&self.#pk_ident)
2027 ),
2028 }
2029 ),
2030 };
2031 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2032 ::std::vec![ #( #after_pairs_pg ),* ];
2033 ::rustango::audit::save_one_with_diff_pool(
2034 pool,
2035 &_query,
2036 #pk_column_lit,
2037 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2038 #pk_value_for_bind,
2039 ),
2040 <Self as ::rustango::core::Model>::SCHEMA.table,
2041 #pk_str,
2042 _after_pairs,
2043 #pg_select_cols,
2044 #my_select_cols,
2045 #sqlite_select_cols,
2046 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
2047 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
2048 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
2049 ).await
2050 }
2051 }
2052 } else {
2053 quote!()
2054 }
2055 } else {
2056 pool_save_method
2057 };
2058
2059 let pool_delete_method = {
2066 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2067 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
2068 if let Some(pk_ident) = pk_ident_for_pool {
2069 if audited_fields.is_some() {
2070 let pairs = audit_pair_tokens.iter();
2071 let pk_str = audit_pk_to_string.clone();
2072 quote! {
2073 pub async fn delete_pool(
2080 &self,
2081 pool: &::rustango::sql::Pool,
2082 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2083 let _query = ::rustango::core::DeleteQuery {
2084 model: <Self as ::rustango::core::Model>::SCHEMA,
2085 where_clause: ::rustango::core::WhereExpr::Predicate(
2086 ::rustango::core::Filter {
2087 column: #pk_column_lit,
2088 op: ::rustango::core::Op::Eq,
2089 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2090 ::core::clone::Clone::clone(&self.#pk_ident)
2091 ),
2092 }
2093 ),
2094 };
2095 let _audit_entry = ::rustango::audit::PendingEntry {
2096 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2097 entity_pk: #pk_str,
2098 operation: ::rustango::audit::AuditOp::Delete,
2099 source: ::rustango::audit::current_source(),
2100 changes: ::rustango::audit::snapshot_changes(&[
2101 #( #pairs ),*
2102 ]),
2103 };
2104 ::rustango::audit::delete_one_with_audit_pool(
2105 pool, &_query, &_audit_entry,
2106 ).await
2107 }
2108 }
2109 } else {
2110 quote! {
2111 pub async fn delete_pool(
2118 &self,
2119 pool: &::rustango::sql::Pool,
2120 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2121 let _query = ::rustango::core::DeleteQuery {
2122 model: <Self as ::rustango::core::Model>::SCHEMA,
2123 where_clause: ::rustango::core::WhereExpr::Predicate(
2124 ::rustango::core::Filter {
2125 column: #pk_column_lit,
2126 op: ::rustango::core::Op::Eq,
2127 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2128 ::core::clone::Clone::clone(&self.#pk_ident)
2129 ),
2130 }
2131 ),
2132 };
2133 ::rustango::sql::delete_pool(pool, &_query).await
2134 }
2135 }
2136 }
2137 } else {
2138 quote!()
2139 }
2140 };
2141
2142 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
2152 audited_fields
2153 {
2154 if tracked.is_empty() {
2155 (quote!(), quote!())
2156 } else {
2157 let select_cols: String = tracked
2158 .iter()
2159 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2160 .collect::<Vec<_>>()
2161 .join(", ");
2162 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
2163 let select_cols_lit = select_cols;
2164 let pk_column_lit_for_select = pk_column_for_select;
2165 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2166 if fields.pk_is_auto {
2167 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2168 } else {
2169 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2170 }
2171 } else {
2172 quote!(0_i64)
2173 };
2174 let before_pairs = tracked.iter().map(|c| {
2175 let column_lit = c.column.as_str();
2176 let value_ty = &c.value_ty;
2177 quote! {
2178 (
2179 #column_lit,
2180 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2181 &_audit_before_row, #column_lit,
2182 ) {
2183 ::core::result::Result::Ok(v) => {
2184 ::serde_json::to_value(&v)
2185 .unwrap_or(::serde_json::Value::Null)
2186 }
2187 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2188 },
2189 )
2190 }
2191 });
2192 let after_pairs = tracked.iter().map(|c| {
2193 let column_lit = c.column.as_str();
2194 let ident = &c.ident;
2195 quote! {
2196 (
2197 #column_lit,
2198 ::serde_json::to_value(&self.#ident)
2199 .unwrap_or(::serde_json::Value::Null),
2200 )
2201 }
2202 });
2203 let pk_str = audit_pk_to_string.clone();
2204 let pre = quote! {
2205 let _audit_select_sql = ::std::format!(
2206 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2207 #select_cols_lit,
2208 <Self as ::rustango::core::Model>::SCHEMA.table,
2209 #pk_column_lit_for_select,
2210 );
2211 let _audit_before_pairs:
2212 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2213 match ::rustango::sql::sqlx::query(&_audit_select_sql)
2214 .bind(#pk_value_for_bind)
2215 .fetch_optional(&mut *_executor)
2216 .await
2217 {
2218 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2219 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2220 }
2221 _ => ::core::option::Option::None,
2222 };
2223 };
2224 let post = quote! {
2225 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2226 let _audit_after:
2227 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2228 ::std::vec![ #( #after_pairs ),* ];
2229 let _audit_entry = ::rustango::audit::PendingEntry {
2230 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2231 entity_pk: #pk_str,
2232 operation: ::rustango::audit::AuditOp::Update,
2233 source: ::rustango::audit::current_source(),
2234 changes: ::rustango::audit::diff_changes(
2235 &_audit_before,
2236 &_audit_after,
2237 ),
2238 };
2239 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2240 }
2241 };
2242 (pre, post)
2243 }
2244 } else {
2245 (quote!(), quote!())
2246 };
2247
2248 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2252 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2253 if fields.pk_is_auto {
2254 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2255 } else {
2256 quote!(::std::format!("{}", &_row.#pk_ident))
2257 }
2258 } else {
2259 quote!(::std::string::String::new())
2260 };
2261 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
2262 let column_lit = c.column.as_str();
2263 let ident = &c.ident;
2264 quote! {
2265 (
2266 #column_lit,
2267 ::serde_json::to_value(&_row.#ident)
2268 .unwrap_or(::serde_json::Value::Null),
2269 )
2270 }
2271 });
2272 quote! {
2273 let _audit_source = ::rustango::audit::current_source();
2274 let mut _audit_entries:
2275 ::std::vec::Vec<::rustango::audit::PendingEntry> =
2276 ::std::vec::Vec::with_capacity(rows.len());
2277 for _row in rows.iter() {
2278 _audit_entries.push(::rustango::audit::PendingEntry {
2279 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2280 entity_pk: #row_pk_str,
2281 operation: ::rustango::audit::AuditOp::Create,
2282 source: _audit_source.clone(),
2283 changes: ::rustango::audit::snapshot_changes(&[
2284 #( #row_pairs ),*
2285 ]),
2286 });
2287 }
2288 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2289 }
2290 } else {
2291 quote!()
2292 };
2293
2294 let save_method = if fields.pk_is_auto {
2295 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
2296 let pk_column_lit = pk_column.as_str();
2297 let assignments = &fields.update_assignments;
2298 let upsert_cols = &fields.upsert_update_columns;
2299 let upsert_pushes = &fields.insert_pushes;
2300 let upsert_returning = &fields.returning_cols;
2301 let upsert_auto_assigns = &fields.auto_assigns;
2302 let upsert_target_columns: Vec<String> = indexes
2311 .iter()
2312 .find(|i| i.unique && !i.columns.is_empty())
2313 .map(|i| i.columns.clone())
2314 .unwrap_or_else(|| vec![pk_column.clone()]);
2315 let upsert_target_lits = upsert_target_columns
2316 .iter()
2317 .map(String::as_str)
2318 .collect::<Vec<_>>();
2319 let conflict_clause = if fields.upsert_update_columns.is_empty() {
2320 quote!(::rustango::core::ConflictClause::DoNothing)
2321 } else {
2322 quote!(::rustango::core::ConflictClause::DoUpdate {
2323 target: ::std::vec![ #( #upsert_target_lits ),* ],
2324 update_columns: ::std::vec![ #( #upsert_cols ),* ],
2325 })
2326 };
2327 Some(quote! {
2328 pub async fn save(
2346 &mut self,
2347 pool: &::rustango::sql::sqlx::PgPool,
2348 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2349 #pool_to_save_on
2350 }
2351
2352 pub async fn save_on #executor_generics (
2363 &mut self,
2364 #executor_param,
2365 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2366 #executor_where
2367 {
2368 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2369 return self.insert_on(#executor_passes_to_data_write).await;
2370 }
2371 #audit_update_pre
2372 let _query = ::rustango::core::UpdateQuery {
2373 model: <Self as ::rustango::core::Model>::SCHEMA,
2374 set: ::std::vec![ #( #assignments ),* ],
2375 where_clause: ::rustango::core::WhereExpr::Predicate(
2376 ::rustango::core::Filter {
2377 column: #pk_column_lit,
2378 op: ::rustango::core::Op::Eq,
2379 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2380 ::core::clone::Clone::clone(&self.#pk_ident)
2381 ),
2382 }
2383 ),
2384 };
2385 let _ = ::rustango::sql::update_on(
2386 #executor_passes_to_data_write,
2387 &_query,
2388 ).await?;
2389 #audit_update_post
2390 ::core::result::Result::Ok(())
2391 }
2392
2393 pub async fn save_on_with #executor_generics (
2404 &mut self,
2405 #executor_param,
2406 source: ::rustango::audit::AuditSource,
2407 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2408 #executor_where
2409 {
2410 ::rustango::audit::with_source(source, self.save_on(_executor)).await
2411 }
2412
2413 pub async fn upsert(
2423 &mut self,
2424 pool: &::rustango::sql::sqlx::PgPool,
2425 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2426 #pool_to_upsert_on
2427 }
2428
2429 pub async fn upsert_on #executor_generics (
2435 &mut self,
2436 #executor_param,
2437 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2438 #executor_where
2439 {
2440 let mut _columns: ::std::vec::Vec<&'static str> =
2441 ::std::vec::Vec::new();
2442 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2443 ::std::vec::Vec::new();
2444 #( #upsert_pushes )*
2445 let query = ::rustango::core::InsertQuery {
2446 model: <Self as ::rustango::core::Model>::SCHEMA,
2447 columns: _columns,
2448 values: _values,
2449 returning: ::std::vec![ #( #upsert_returning ),* ],
2450 on_conflict: ::core::option::Option::Some(#conflict_clause),
2451 };
2452 let _returning_row_v = ::rustango::sql::insert_returning_on(
2453 #executor_passes_to_data_write,
2454 &query,
2455 ).await?;
2456 let _returning_row = &_returning_row_v;
2457 #( #upsert_auto_assigns )*
2458 ::core::result::Result::Ok(())
2459 }
2460 })
2461 } else {
2462 None
2463 };
2464
2465 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2466 let pk_column_lit = pk_column.as_str();
2467 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2474 let col_lit = col;
2475 quote! {
2476 pub async fn soft_delete_on #executor_generics (
2486 &self,
2487 #executor_param,
2488 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2489 #executor_where
2490 {
2491 let _query = ::rustango::core::UpdateQuery {
2492 model: <Self as ::rustango::core::Model>::SCHEMA,
2493 set: ::std::vec![
2494 ::rustango::core::Assignment {
2495 column: #col_lit,
2496 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2497 ::chrono::Utc::now()
2498 ),
2499 },
2500 ],
2501 where_clause: ::rustango::core::WhereExpr::Predicate(
2502 ::rustango::core::Filter {
2503 column: #pk_column_lit,
2504 op: ::rustango::core::Op::Eq,
2505 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2506 ::core::clone::Clone::clone(&self.#pk_ident)
2507 ),
2508 }
2509 ),
2510 };
2511 let _affected = ::rustango::sql::update_on(
2512 #executor_passes_to_data_write,
2513 &_query,
2514 ).await?;
2515 #audit_softdelete_emit
2516 ::core::result::Result::Ok(_affected)
2517 }
2518
2519 pub async fn restore_on #executor_generics (
2526 &self,
2527 #executor_param,
2528 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2529 #executor_where
2530 {
2531 let _query = ::rustango::core::UpdateQuery {
2532 model: <Self as ::rustango::core::Model>::SCHEMA,
2533 set: ::std::vec![
2534 ::rustango::core::Assignment {
2535 column: #col_lit,
2536 value: ::rustango::core::SqlValue::Null,
2537 },
2538 ],
2539 where_clause: ::rustango::core::WhereExpr::Predicate(
2540 ::rustango::core::Filter {
2541 column: #pk_column_lit,
2542 op: ::rustango::core::Op::Eq,
2543 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2544 ::core::clone::Clone::clone(&self.#pk_ident)
2545 ),
2546 }
2547 ),
2548 };
2549 let _affected = ::rustango::sql::update_on(
2550 #executor_passes_to_data_write,
2551 &_query,
2552 ).await?;
2553 #audit_restore_emit
2554 ::core::result::Result::Ok(_affected)
2555 }
2556 }
2557 } else {
2558 quote!()
2559 };
2560 quote! {
2561 pub async fn delete(
2569 &self,
2570 pool: &::rustango::sql::sqlx::PgPool,
2571 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2572 #pool_to_delete_on
2573 }
2574
2575 pub async fn delete_on #executor_generics (
2582 &self,
2583 #executor_param,
2584 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2585 #executor_where
2586 {
2587 let query = ::rustango::core::DeleteQuery {
2588 model: <Self as ::rustango::core::Model>::SCHEMA,
2589 where_clause: ::rustango::core::WhereExpr::Predicate(
2590 ::rustango::core::Filter {
2591 column: #pk_column_lit,
2592 op: ::rustango::core::Op::Eq,
2593 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2594 ::core::clone::Clone::clone(&self.#pk_ident)
2595 ),
2596 }
2597 ),
2598 };
2599 let _affected = ::rustango::sql::delete_on(
2600 #executor_passes_to_data_write,
2601 &query,
2602 ).await?;
2603 #audit_delete_emit
2604 ::core::result::Result::Ok(_affected)
2605 }
2606
2607 pub async fn delete_on_with #executor_generics (
2613 &self,
2614 #executor_param,
2615 source: ::rustango::audit::AuditSource,
2616 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2617 #executor_where
2618 {
2619 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2620 }
2621 #pool_delete_method
2622 #pool_insert_method
2623 #pool_save_method
2624 #soft_delete_methods
2625 }
2626 });
2627
2628 let insert_method = if fields.has_auto {
2629 let pushes = &fields.insert_pushes;
2630 let returning_cols = &fields.returning_cols;
2631 let auto_assigns = &fields.auto_assigns;
2632 quote! {
2633 pub async fn insert(
2642 &mut self,
2643 pool: &::rustango::sql::sqlx::PgPool,
2644 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2645 #pool_to_insert_on
2646 }
2647
2648 pub async fn insert_on #executor_generics (
2654 &mut self,
2655 #executor_param,
2656 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2657 #executor_where
2658 {
2659 let mut _columns: ::std::vec::Vec<&'static str> =
2660 ::std::vec::Vec::new();
2661 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2662 ::std::vec::Vec::new();
2663 #( #pushes )*
2664 let query = ::rustango::core::InsertQuery {
2665 model: <Self as ::rustango::core::Model>::SCHEMA,
2666 columns: _columns,
2667 values: _values,
2668 returning: ::std::vec![ #( #returning_cols ),* ],
2669 on_conflict: ::core::option::Option::None,
2670 };
2671 let _returning_row_v = ::rustango::sql::insert_returning_on(
2672 #executor_passes_to_data_write,
2673 &query,
2674 ).await?;
2675 let _returning_row = &_returning_row_v;
2676 #( #auto_assigns )*
2677 #audit_insert_emit
2678 ::core::result::Result::Ok(())
2679 }
2680
2681 pub async fn insert_on_with #executor_generics (
2687 &mut self,
2688 #executor_param,
2689 source: ::rustango::audit::AuditSource,
2690 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2691 #executor_where
2692 {
2693 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2694 }
2695 }
2696 } else {
2697 let insert_columns = &fields.insert_columns;
2698 let insert_values = &fields.insert_values;
2699 quote! {
2700 pub async fn insert(
2706 &self,
2707 pool: &::rustango::sql::sqlx::PgPool,
2708 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2709 self.insert_on(pool).await
2710 }
2711
2712 pub async fn insert_on<'_c, _E>(
2718 &self,
2719 _executor: _E,
2720 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2721 where
2722 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2723 {
2724 let query = ::rustango::core::InsertQuery {
2725 model: <Self as ::rustango::core::Model>::SCHEMA,
2726 columns: ::std::vec![ #( #insert_columns ),* ],
2727 values: ::std::vec![ #( #insert_values ),* ],
2728 returning: ::std::vec::Vec::new(),
2729 on_conflict: ::core::option::Option::None,
2730 };
2731 ::rustango::sql::insert_on(_executor, &query).await
2732 }
2733 }
2734 };
2735
2736 let bulk_insert_method = if fields.has_auto {
2737 let cols_no_auto = &fields.bulk_columns_no_auto;
2738 let cols_all = &fields.bulk_columns_all;
2739 let pushes_no_auto = &fields.bulk_pushes_no_auto;
2740 let pushes_all = &fields.bulk_pushes_all;
2741 let returning_cols = &fields.returning_cols;
2742 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2743 let uniformity = &fields.bulk_auto_uniformity;
2744 let first_auto_ident = fields
2745 .first_auto_ident
2746 .as_ref()
2747 .expect("has_auto implies first_auto_ident is Some");
2748 quote! {
2749 pub async fn bulk_insert(
2763 rows: &mut [Self],
2764 pool: &::rustango::sql::sqlx::PgPool,
2765 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2766 #pool_to_bulk_insert_on
2767 }
2768
2769 pub async fn bulk_insert_on #executor_generics (
2775 rows: &mut [Self],
2776 #executor_param,
2777 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2778 #executor_where
2779 {
2780 if rows.is_empty() {
2781 return ::core::result::Result::Ok(());
2782 }
2783 let _first_unset = matches!(
2784 rows[0].#first_auto_ident,
2785 ::rustango::sql::Auto::Unset
2786 );
2787 #( #uniformity )*
2788
2789 let mut _all_rows: ::std::vec::Vec<
2790 ::std::vec::Vec<::rustango::core::SqlValue>,
2791 > = ::std::vec::Vec::with_capacity(rows.len());
2792 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2793 for _row in rows.iter() {
2794 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2795 ::std::vec::Vec::new();
2796 #( #pushes_no_auto )*
2797 _all_rows.push(_row_vals);
2798 }
2799 ::std::vec![ #( #cols_no_auto ),* ]
2800 } else {
2801 for _row in rows.iter() {
2802 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2803 ::std::vec::Vec::new();
2804 #( #pushes_all )*
2805 _all_rows.push(_row_vals);
2806 }
2807 ::std::vec![ #( #cols_all ),* ]
2808 };
2809
2810 let _query = ::rustango::core::BulkInsertQuery {
2811 model: <Self as ::rustango::core::Model>::SCHEMA,
2812 columns: _columns,
2813 rows: _all_rows,
2814 returning: ::std::vec![ #( #returning_cols ),* ],
2815 on_conflict: ::core::option::Option::None,
2816 };
2817 let _returned = ::rustango::sql::bulk_insert_on(
2818 #executor_passes_to_data_write,
2819 &_query,
2820 ).await?;
2821 if _returned.len() != rows.len() {
2822 return ::core::result::Result::Err(
2823 ::rustango::sql::ExecError::Sql(
2824 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2825 expected: rows.len(),
2826 actual: _returned.len(),
2827 }
2828 )
2829 );
2830 }
2831 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2832 #auto_assigns_for_row
2833 }
2834 #audit_bulk_insert_emit
2835 ::core::result::Result::Ok(())
2836 }
2837 }
2838 } else {
2839 let cols_all = &fields.bulk_columns_all;
2840 let pushes_all = &fields.bulk_pushes_all;
2841 quote! {
2842 pub async fn bulk_insert(
2852 rows: &[Self],
2853 pool: &::rustango::sql::sqlx::PgPool,
2854 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2855 Self::bulk_insert_on(rows, pool).await
2856 }
2857
2858 pub async fn bulk_insert_on<'_c, _E>(
2864 rows: &[Self],
2865 _executor: _E,
2866 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2867 where
2868 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2869 {
2870 if rows.is_empty() {
2871 return ::core::result::Result::Ok(());
2872 }
2873 let mut _all_rows: ::std::vec::Vec<
2874 ::std::vec::Vec<::rustango::core::SqlValue>,
2875 > = ::std::vec::Vec::with_capacity(rows.len());
2876 for _row in rows.iter() {
2877 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2878 ::std::vec::Vec::new();
2879 #( #pushes_all )*
2880 _all_rows.push(_row_vals);
2881 }
2882 let _query = ::rustango::core::BulkInsertQuery {
2883 model: <Self as ::rustango::core::Model>::SCHEMA,
2884 columns: ::std::vec![ #( #cols_all ),* ],
2885 rows: _all_rows,
2886 returning: ::std::vec::Vec::new(),
2887 on_conflict: ::core::option::Option::None,
2888 };
2889 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2890 ::core::result::Result::Ok(())
2891 }
2892 }
2893 };
2894
2895 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2896 quote! {
2897 #[doc(hidden)]
2902 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2903 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2904 ::core::clone::Clone::clone(&self.#pk_ident)
2905 )
2906 }
2907 }
2908 });
2909
2910 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2911 quote! {
2912 impl ::rustango::sql::HasPkValue for #struct_name {
2913 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2914 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2915 ::core::clone::Clone::clone(&self.#pk_ident)
2916 )
2917 }
2918 }
2919 }
2920 });
2921
2922 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2923
2924 let assign_auto_pk_pool_impl = {
2930 let auto_assigns = &fields.auto_assigns;
2931 let auto_assigns_sqlite: Vec<TokenStream2> = fields
2937 .auto_field_idents
2938 .iter()
2939 .map(|(ident, column)| {
2940 quote! {
2941 self.#ident = ::rustango::sql::try_get_returning_sqlite(
2942 _returning_row, #column
2943 )?;
2944 }
2945 })
2946 .collect();
2947 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
2948 let value_ty = fields
2966 .first_auto_value_ty
2967 .as_ref()
2968 .expect("first_auto_value_ty set whenever first_auto_ident is");
2969 quote! {
2970 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
2971 ::rustango_from_mysql_auto_id(_id)?;
2972 self.#first = ::rustango::sql::Auto::Set(_converted);
2973 ::core::result::Result::Ok(())
2974 }
2975 } else {
2976 quote! {
2977 let _ = _id;
2978 ::core::result::Result::Ok(())
2979 }
2980 };
2981 quote! {
2982 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
2983 fn __rustango_assign_from_pg_row(
2984 &mut self,
2985 _returning_row: &::rustango::sql::PgReturningRow,
2986 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2987 #( #auto_assigns )*
2988 ::core::result::Result::Ok(())
2989 }
2990 fn __rustango_assign_from_mysql_id(
2991 &mut self,
2992 _id: i64,
2993 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2994 #mysql_body
2995 }
2996 fn __rustango_assign_from_sqlite_row(
2997 &mut self,
2998 _returning_row: &::rustango::sql::SqliteReturningRow,
2999 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3000 #( #auto_assigns_sqlite )*
3001 ::core::result::Result::Ok(())
3002 }
3003 }
3004 }
3005 };
3006
3007 let from_aliased_row_inits = &fields.from_aliased_row_inits;
3008 let aliased_row_helper = quote! {
3009 #[doc(hidden)]
3015 pub fn __rustango_from_aliased_row(
3016 row: &::rustango::sql::sqlx::postgres::PgRow,
3017 prefix: &str,
3018 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3019 ::core::result::Result::Ok(Self {
3020 #( #from_aliased_row_inits ),*
3021 })
3022 }
3023 };
3024 let aliased_row_helper_my = quote! {
3027 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
3028 #( #from_aliased_row_inits ),*
3029 });
3030 };
3031
3032 let aliased_row_helper_sqlite = quote! {
3035 ::rustango::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
3036 #( #from_aliased_row_inits ),*
3037 });
3038 };
3039
3040 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
3041 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
3042 let load_related_impl_sqlite =
3043 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
3044
3045 quote! {
3046 impl #struct_name {
3047 #[must_use]
3049 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
3050 ::rustango::query::QuerySet::new()
3051 }
3052
3053 #insert_method
3054
3055 #bulk_insert_method
3056
3057 #save_method
3058
3059 #pk_methods
3060
3061 #pk_value_helper
3062
3063 #aliased_row_helper
3064
3065 #column_consts
3066 }
3067
3068 #aliased_row_helper_my
3069
3070 #aliased_row_helper_sqlite
3071
3072 #load_related_impl
3073
3074 #load_related_impl_my
3075
3076 #load_related_impl_sqlite
3077
3078 #has_pk_value_impl
3079
3080 #fk_pk_access_impl
3081
3082 #assign_auto_pk_pool_impl
3083 }
3084}
3085
3086fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
3090 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
3091 let col_lit = column.as_str();
3092 quote! {
3093 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
3094 _returning_row,
3095 #col_lit,
3096 )?;
3097 }
3098 });
3099 quote! { #( #lines )* }
3100}
3101
3102fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
3104 let lines = entries.iter().map(|e| {
3105 let ident = &e.ident;
3106 let col_ty = column_type_ident(ident);
3107 quote! {
3108 #[allow(non_upper_case_globals)]
3109 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
3110 }
3111 });
3112 quote! { #(#lines)* }
3113}
3114
3115fn column_module_tokens(
3118 module_ident: &syn::Ident,
3119 struct_name: &syn::Ident,
3120 entries: &[ColumnEntry],
3121) -> TokenStream2 {
3122 let items = entries.iter().map(|e| {
3123 let col_ty = column_type_ident(&e.ident);
3124 let value_ty = &e.value_ty;
3125 let name = &e.name;
3126 let column = &e.column;
3127 let field_type_tokens = &e.field_type_tokens;
3128 quote! {
3129 #[derive(::core::clone::Clone, ::core::marker::Copy)]
3130 pub struct #col_ty;
3131
3132 impl ::rustango::core::Column for #col_ty {
3133 type Model = super::#struct_name;
3134 type Value = #value_ty;
3135 const NAME: &'static str = #name;
3136 const COLUMN: &'static str = #column;
3137 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
3138 }
3139 }
3140 });
3141 quote! {
3142 #[doc(hidden)]
3143 #[allow(non_camel_case_types, non_snake_case)]
3144 pub mod #module_ident {
3145 #[allow(unused_imports)]
3150 use super::*;
3151 #(#items)*
3152 }
3153 }
3154}
3155
3156fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
3157 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
3158}
3159
3160fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3161 syn::Ident::new(
3162 &format!("__rustango_cols_{struct_name}"),
3163 struct_name.span(),
3164 )
3165}
3166
3167fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3168 quote! {
3179 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3180 for #struct_name
3181 {
3182 fn from_row(
3183 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3184 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3185 ::core::result::Result::Ok(Self {
3186 #( #from_row_inits ),*
3187 })
3188 }
3189 }
3190
3191 ::rustango::__impl_my_from_row!(#struct_name, |row| {
3192 #( #from_row_inits ),*
3193 });
3194
3195 ::rustango::__impl_sqlite_from_row!(#struct_name, |row| {
3196 #( #from_row_inits ),*
3197 });
3198 }
3199}
3200
3201struct ContainerAttrs {
3202 table: Option<String>,
3203 display: Option<(String, proc_macro2::Span)>,
3204 app: Option<String>,
3211 admin: Option<AdminAttrs>,
3216 audit: Option<AuditAttrs>,
3222 permissions: bool,
3226 m2m: Vec<M2MAttr>,
3230 indexes: Vec<IndexAttr>,
3236 checks: Vec<CheckAttr>,
3239 composite_fks: Vec<CompositeFkAttr>,
3243 generic_fks: Vec<GenericFkAttr>,
3247 scope: Option<String>,
3253}
3254
3255struct IndexAttr {
3257 name: Option<String>,
3259 columns: Vec<String>,
3261 unique: bool,
3263}
3264
3265struct CheckAttr {
3267 name: String,
3268 expr: String,
3269}
3270
3271struct CompositeFkAttr {
3277 name: String,
3279 to: String,
3281 from: Vec<String>,
3283 on: Vec<String>,
3285}
3286
3287struct GenericFkAttr {
3292 name: String,
3294 ct_column: String,
3296 pk_column: String,
3298}
3299
3300struct M2MAttr {
3302 name: String,
3304 to: String,
3306 through: String,
3308 src: String,
3310 dst: String,
3312}
3313
3314#[derive(Default)]
3320struct AuditAttrs {
3321 track: Option<(Vec<String>, proc_macro2::Span)>,
3325}
3326
3327#[derive(Default)]
3332struct AdminAttrs {
3333 list_display: Option<(Vec<String>, proc_macro2::Span)>,
3334 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3335 list_per_page: Option<usize>,
3336 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3337 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3338 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3339 actions: Option<(Vec<String>, proc_macro2::Span)>,
3342 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3346}
3347
3348fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3349 let mut out = ContainerAttrs {
3350 table: None,
3351 display: None,
3352 app: None,
3353 admin: None,
3354 audit: None,
3355 permissions: true,
3364 m2m: Vec::new(),
3365 indexes: Vec::new(),
3366 checks: Vec::new(),
3367 composite_fks: Vec::new(),
3368 generic_fks: Vec::new(),
3369 scope: None,
3370 };
3371 for attr in &input.attrs {
3372 if !attr.path().is_ident("rustango") {
3373 continue;
3374 }
3375 attr.parse_nested_meta(|meta| {
3376 if meta.path.is_ident("table") {
3377 let s: LitStr = meta.value()?.parse()?;
3378 let name = s.value();
3379 validate_table_name(&name, s.span())?;
3389 out.table = Some(name);
3390 return Ok(());
3391 }
3392 if meta.path.is_ident("display") {
3393 let s: LitStr = meta.value()?.parse()?;
3394 out.display = Some((s.value(), s.span()));
3395 return Ok(());
3396 }
3397 if meta.path.is_ident("app") {
3398 let s: LitStr = meta.value()?.parse()?;
3399 out.app = Some(s.value());
3400 return Ok(());
3401 }
3402 if meta.path.is_ident("scope") {
3403 let s: LitStr = meta.value()?.parse()?;
3404 let val = s.value();
3405 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
3406 return Err(meta.error(format!(
3407 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
3408 )));
3409 }
3410 out.scope = Some(val);
3411 return Ok(());
3412 }
3413 if meta.path.is_ident("admin") {
3414 let mut admin = AdminAttrs::default();
3415 meta.parse_nested_meta(|inner| {
3416 if inner.path.is_ident("list_display") {
3417 let s: LitStr = inner.value()?.parse()?;
3418 admin.list_display =
3419 Some((split_field_list(&s.value()), s.span()));
3420 return Ok(());
3421 }
3422 if inner.path.is_ident("search_fields") {
3423 let s: LitStr = inner.value()?.parse()?;
3424 admin.search_fields =
3425 Some((split_field_list(&s.value()), s.span()));
3426 return Ok(());
3427 }
3428 if inner.path.is_ident("readonly_fields") {
3429 let s: LitStr = inner.value()?.parse()?;
3430 admin.readonly_fields =
3431 Some((split_field_list(&s.value()), s.span()));
3432 return Ok(());
3433 }
3434 if inner.path.is_ident("list_per_page") {
3435 let lit: syn::LitInt = inner.value()?.parse()?;
3436 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3437 return Ok(());
3438 }
3439 if inner.path.is_ident("ordering") {
3440 let s: LitStr = inner.value()?.parse()?;
3441 admin.ordering = Some((
3442 parse_ordering_list(&s.value()),
3443 s.span(),
3444 ));
3445 return Ok(());
3446 }
3447 if inner.path.is_ident("list_filter") {
3448 let s: LitStr = inner.value()?.parse()?;
3449 admin.list_filter =
3450 Some((split_field_list(&s.value()), s.span()));
3451 return Ok(());
3452 }
3453 if inner.path.is_ident("actions") {
3454 let s: LitStr = inner.value()?.parse()?;
3455 admin.actions =
3456 Some((split_field_list(&s.value()), s.span()));
3457 return Ok(());
3458 }
3459 if inner.path.is_ident("fieldsets") {
3460 let s: LitStr = inner.value()?.parse()?;
3461 admin.fieldsets =
3462 Some((parse_fieldset_list(&s.value()), s.span()));
3463 return Ok(());
3464 }
3465 Err(inner.error(
3466 "unknown admin attribute (supported: \
3467 `list_display`, `search_fields`, `readonly_fields`, \
3468 `list_filter`, `list_per_page`, `ordering`, `actions`, \
3469 `fieldsets`)",
3470 ))
3471 })?;
3472 out.admin = Some(admin);
3473 return Ok(());
3474 }
3475 if meta.path.is_ident("audit") {
3476 let mut audit = AuditAttrs::default();
3477 meta.parse_nested_meta(|inner| {
3478 if inner.path.is_ident("track") {
3479 let s: LitStr = inner.value()?.parse()?;
3480 audit.track =
3481 Some((split_field_list(&s.value()), s.span()));
3482 return Ok(());
3483 }
3484 Err(inner.error(
3485 "unknown audit attribute (supported: `track`)",
3486 ))
3487 })?;
3488 out.audit = Some(audit);
3489 return Ok(());
3490 }
3491 if meta.path.is_ident("permissions") {
3492 if let Ok(v) = meta.value() {
3497 let lit: syn::LitBool = v.parse()?;
3498 out.permissions = lit.value;
3499 } else {
3500 out.permissions = true;
3501 }
3502 return Ok(());
3503 }
3504 if meta.path.is_ident("unique_together") {
3505 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3514 out.indexes.push(IndexAttr { name, columns, unique: true });
3515 return Ok(());
3516 }
3517 if meta.path.is_ident("index_together") {
3518 let (columns, name) = parse_together_attr(&meta, "index_together")?;
3524 out.indexes.push(IndexAttr { name, columns, unique: false });
3525 return Ok(());
3526 }
3527 if meta.path.is_ident("index") {
3528 let cols_lit: LitStr = meta.value()?.parse()?;
3536 let columns = split_field_list(&cols_lit.value());
3537 out.indexes.push(IndexAttr { name: None, columns, unique: false });
3538 return Ok(());
3539 }
3540 if meta.path.is_ident("check") {
3541 let mut name: Option<String> = None;
3543 let mut expr: Option<String> = None;
3544 meta.parse_nested_meta(|inner| {
3545 if inner.path.is_ident("name") {
3546 let s: LitStr = inner.value()?.parse()?;
3547 name = Some(s.value());
3548 return Ok(());
3549 }
3550 if inner.path.is_ident("expr") {
3551 let s: LitStr = inner.value()?.parse()?;
3552 expr = Some(s.value());
3553 return Ok(());
3554 }
3555 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3556 })?;
3557 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3558 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3559 out.checks.push(CheckAttr { name, expr });
3560 return Ok(());
3561 }
3562 if meta.path.is_ident("generic_fk") {
3563 let mut gfk = GenericFkAttr {
3564 name: String::new(),
3565 ct_column: String::new(),
3566 pk_column: String::new(),
3567 };
3568 meta.parse_nested_meta(|inner| {
3569 if inner.path.is_ident("name") {
3570 let s: LitStr = inner.value()?.parse()?;
3571 gfk.name = s.value();
3572 return Ok(());
3573 }
3574 if inner.path.is_ident("ct_column") {
3575 let s: LitStr = inner.value()?.parse()?;
3576 gfk.ct_column = s.value();
3577 return Ok(());
3578 }
3579 if inner.path.is_ident("pk_column") {
3580 let s: LitStr = inner.value()?.parse()?;
3581 gfk.pk_column = s.value();
3582 return Ok(());
3583 }
3584 Err(inner.error(
3585 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3586 ))
3587 })?;
3588 if gfk.name.is_empty() {
3589 return Err(meta.error("generic_fk requires `name = \"...\"`"));
3590 }
3591 if gfk.ct_column.is_empty() {
3592 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3593 }
3594 if gfk.pk_column.is_empty() {
3595 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3596 }
3597 out.generic_fks.push(gfk);
3598 return Ok(());
3599 }
3600 if meta.path.is_ident("fk_composite") {
3601 let mut fk = CompositeFkAttr {
3602 name: String::new(),
3603 to: String::new(),
3604 from: Vec::new(),
3605 on: Vec::new(),
3606 };
3607 meta.parse_nested_meta(|inner| {
3608 if inner.path.is_ident("name") {
3609 let s: LitStr = inner.value()?.parse()?;
3610 fk.name = s.value();
3611 return Ok(());
3612 }
3613 if inner.path.is_ident("to") {
3614 let s: LitStr = inner.value()?.parse()?;
3615 fk.to = s.value();
3616 return Ok(());
3617 }
3618 if inner.path.is_ident("on") || inner.path.is_ident("from") {
3621 let value = inner.value()?;
3622 let content;
3623 syn::parenthesized!(content in value);
3624 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3625 content.parse_terminated(
3626 |p| p.parse::<syn::LitStr>(),
3627 syn::Token![,],
3628 )?;
3629 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3630 if inner.path.is_ident("on") {
3631 fk.on = cols;
3632 } else {
3633 fk.from = cols;
3634 }
3635 return Ok(());
3636 }
3637 Err(inner.error(
3638 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3639 ))
3640 })?;
3641 if fk.name.is_empty() {
3642 return Err(meta.error("fk_composite requires `name = \"...\"`"));
3643 }
3644 if fk.to.is_empty() {
3645 return Err(meta.error("fk_composite requires `to = \"...\"`"));
3646 }
3647 if fk.from.is_empty() || fk.on.is_empty() {
3648 return Err(meta.error(
3649 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3650 ));
3651 }
3652 if fk.from.len() != fk.on.len() {
3653 return Err(meta.error(format!(
3654 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3655 fk.from.len(),
3656 fk.on.len(),
3657 )));
3658 }
3659 out.composite_fks.push(fk);
3660 return Ok(());
3661 }
3662 if meta.path.is_ident("m2m") {
3663 let mut m2m = M2MAttr {
3664 name: String::new(),
3665 to: String::new(),
3666 through: String::new(),
3667 src: String::new(),
3668 dst: String::new(),
3669 };
3670 meta.parse_nested_meta(|inner| {
3671 if inner.path.is_ident("name") {
3672 let s: LitStr = inner.value()?.parse()?;
3673 m2m.name = s.value();
3674 return Ok(());
3675 }
3676 if inner.path.is_ident("to") {
3677 let s: LitStr = inner.value()?.parse()?;
3678 m2m.to = s.value();
3679 return Ok(());
3680 }
3681 if inner.path.is_ident("through") {
3682 let s: LitStr = inner.value()?.parse()?;
3683 m2m.through = s.value();
3684 return Ok(());
3685 }
3686 if inner.path.is_ident("src") {
3687 let s: LitStr = inner.value()?.parse()?;
3688 m2m.src = s.value();
3689 return Ok(());
3690 }
3691 if inner.path.is_ident("dst") {
3692 let s: LitStr = inner.value()?.parse()?;
3693 m2m.dst = s.value();
3694 return Ok(());
3695 }
3696 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3697 })?;
3698 if m2m.name.is_empty() {
3699 return Err(meta.error("m2m requires `name = \"...\"`"));
3700 }
3701 if m2m.to.is_empty() {
3702 return Err(meta.error("m2m requires `to = \"...\"`"));
3703 }
3704 if m2m.through.is_empty() {
3705 return Err(meta.error("m2m requires `through = \"...\"`"));
3706 }
3707 if m2m.src.is_empty() {
3708 return Err(meta.error("m2m requires `src = \"...\"`"));
3709 }
3710 if m2m.dst.is_empty() {
3711 return Err(meta.error("m2m requires `dst = \"...\"`"));
3712 }
3713 out.m2m.push(m2m);
3714 return Ok(());
3715 }
3716 Err(meta.error("unknown rustango container attribute"))
3717 })?;
3718 }
3719 Ok(out)
3720}
3721
3722fn split_field_list(raw: &str) -> Vec<String> {
3726 raw.split(',')
3727 .map(str::trim)
3728 .filter(|s| !s.is_empty())
3729 .map(str::to_owned)
3730 .collect()
3731}
3732
3733fn parse_together_attr(
3741 meta: &syn::meta::ParseNestedMeta<'_>,
3742 attr: &str,
3743) -> syn::Result<(Vec<String>, Option<String>)> {
3744 if meta.input.peek(syn::Token![=]) {
3747 let cols_lit: LitStr = meta.value()?.parse()?;
3748 let columns = split_field_list(&cols_lit.value());
3749 check_together_columns(meta, attr, &columns)?;
3750 return Ok((columns, None));
3751 }
3752 let mut columns: Option<Vec<String>> = None;
3753 let mut name: Option<String> = None;
3754 meta.parse_nested_meta(|inner| {
3755 if inner.path.is_ident("columns") {
3756 let s: LitStr = inner.value()?.parse()?;
3757 columns = Some(split_field_list(&s.value()));
3758 return Ok(());
3759 }
3760 if inner.path.is_ident("name") {
3761 let s: LitStr = inner.value()?.parse()?;
3762 name = Some(s.value());
3763 return Ok(());
3764 }
3765 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3766 })?;
3767 let columns = columns.ok_or_else(|| {
3768 meta.error(format!(
3769 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3770 ))
3771 })?;
3772 check_together_columns(meta, attr, &columns)?;
3773 Ok((columns, name))
3774}
3775
3776fn check_together_columns(
3777 meta: &syn::meta::ParseNestedMeta<'_>,
3778 attr: &str,
3779 columns: &[String],
3780) -> syn::Result<()> {
3781 if columns.len() < 2 {
3782 let single = if attr == "unique_together" {
3783 "#[rustango(unique)] on the field"
3784 } else {
3785 "#[rustango(index)] on the field"
3786 };
3787 return Err(meta.error(format!(
3788 "{attr} expects two or more columns; for a single-column equivalent use {single}",
3789 )));
3790 }
3791 Ok(())
3792}
3793
3794fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3803 raw.split('|')
3804 .map(str::trim)
3805 .filter(|s| !s.is_empty())
3806 .map(|section| {
3807 let (title, rest) = match section.split_once(':') {
3809 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
3810 _ => (String::new(), section),
3811 };
3812 let fields = split_field_list(rest);
3813 (title, fields)
3814 })
3815 .collect()
3816}
3817
3818fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3821 raw.split(',')
3822 .map(str::trim)
3823 .filter(|s| !s.is_empty())
3824 .map(|spec| {
3825 spec.strip_prefix('-')
3826 .map_or((spec.to_owned(), false), |rest| {
3827 (rest.trim().to_owned(), true)
3828 })
3829 })
3830 .collect()
3831}
3832
3833struct FieldAttrs {
3834 column: Option<String>,
3835 primary_key: bool,
3836 fk: Option<String>,
3837 o2o: Option<String>,
3838 on: Option<String>,
3839 max_length: Option<u32>,
3840 min: Option<i64>,
3841 max: Option<i64>,
3842 default: Option<String>,
3843 auto_uuid: bool,
3849 auto_now_add: bool,
3854 auto_now: bool,
3860 soft_delete: bool,
3865 unique: bool,
3868 index: bool,
3872 index_unique: bool,
3873 index_name: Option<String>,
3874 generated_as: Option<String>,
3880}
3881
3882fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3883 let mut out = FieldAttrs {
3884 column: None,
3885 primary_key: false,
3886 fk: None,
3887 o2o: None,
3888 on: None,
3889 max_length: None,
3890 min: None,
3891 max: None,
3892 default: None,
3893 auto_uuid: false,
3894 auto_now_add: false,
3895 auto_now: false,
3896 soft_delete: false,
3897 unique: false,
3898 index: false,
3899 index_unique: false,
3900 index_name: None,
3901 generated_as: None,
3902 };
3903 for attr in &field.attrs {
3904 if !attr.path().is_ident("rustango") {
3905 continue;
3906 }
3907 attr.parse_nested_meta(|meta| {
3908 if meta.path.is_ident("column") {
3909 let s: LitStr = meta.value()?.parse()?;
3910 out.column = Some(s.value());
3911 return Ok(());
3912 }
3913 if meta.path.is_ident("primary_key") {
3914 out.primary_key = true;
3915 return Ok(());
3916 }
3917 if meta.path.is_ident("fk") {
3918 let s: LitStr = meta.value()?.parse()?;
3919 out.fk = Some(s.value());
3920 return Ok(());
3921 }
3922 if meta.path.is_ident("o2o") {
3923 let s: LitStr = meta.value()?.parse()?;
3924 out.o2o = Some(s.value());
3925 return Ok(());
3926 }
3927 if meta.path.is_ident("on") {
3928 let s: LitStr = meta.value()?.parse()?;
3929 out.on = Some(s.value());
3930 return Ok(());
3931 }
3932 if meta.path.is_ident("max_length") {
3933 let lit: syn::LitInt = meta.value()?.parse()?;
3934 out.max_length = Some(lit.base10_parse::<u32>()?);
3935 return Ok(());
3936 }
3937 if meta.path.is_ident("min") {
3938 out.min = Some(parse_signed_i64(&meta)?);
3939 return Ok(());
3940 }
3941 if meta.path.is_ident("max") {
3942 out.max = Some(parse_signed_i64(&meta)?);
3943 return Ok(());
3944 }
3945 if meta.path.is_ident("default") {
3946 let s: LitStr = meta.value()?.parse()?;
3947 out.default = Some(s.value());
3948 return Ok(());
3949 }
3950 if meta.path.is_ident("generated_as") {
3951 let s: LitStr = meta.value()?.parse()?;
3952 out.generated_as = Some(s.value());
3953 return Ok(());
3954 }
3955 if meta.path.is_ident("auto_uuid") {
3956 out.auto_uuid = true;
3957 out.primary_key = true;
3961 if out.default.is_none() {
3962 out.default = Some("gen_random_uuid()".into());
3963 }
3964 return Ok(());
3965 }
3966 if meta.path.is_ident("auto_now_add") {
3967 out.auto_now_add = true;
3968 if out.default.is_none() {
3969 out.default = Some("now()".into());
3970 }
3971 return Ok(());
3972 }
3973 if meta.path.is_ident("auto_now") {
3974 out.auto_now = true;
3975 if out.default.is_none() {
3976 out.default = Some("now()".into());
3977 }
3978 return Ok(());
3979 }
3980 if meta.path.is_ident("soft_delete") {
3981 out.soft_delete = true;
3982 return Ok(());
3983 }
3984 if meta.path.is_ident("unique") {
3985 out.unique = true;
3986 return Ok(());
3987 }
3988 if meta.path.is_ident("index") {
3989 out.index = true;
3990 if meta.input.peek(syn::token::Paren) {
3992 meta.parse_nested_meta(|inner| {
3993 if inner.path.is_ident("unique") {
3994 out.index_unique = true;
3995 return Ok(());
3996 }
3997 if inner.path.is_ident("name") {
3998 let s: LitStr = inner.value()?.parse()?;
3999 out.index_name = Some(s.value());
4000 return Ok(());
4001 }
4002 Err(inner
4003 .error("unknown index sub-attribute (supported: `unique`, `name`)"))
4004 })?;
4005 }
4006 return Ok(());
4007 }
4008 Err(meta.error("unknown rustango field attribute"))
4009 })?;
4010 }
4011 Ok(out)
4012}
4013
4014fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
4016 let expr: syn::Expr = meta.value()?.parse()?;
4017 match expr {
4018 syn::Expr::Lit(syn::ExprLit {
4019 lit: syn::Lit::Int(lit),
4020 ..
4021 }) => lit.base10_parse::<i64>(),
4022 syn::Expr::Unary(syn::ExprUnary {
4023 op: syn::UnOp::Neg(_),
4024 expr,
4025 ..
4026 }) => {
4027 if let syn::Expr::Lit(syn::ExprLit {
4028 lit: syn::Lit::Int(lit),
4029 ..
4030 }) = *expr
4031 {
4032 let v: i64 = lit.base10_parse()?;
4033 Ok(-v)
4034 } else {
4035 Err(syn::Error::new_spanned(expr, "expected integer literal"))
4036 }
4037 }
4038 other => Err(syn::Error::new_spanned(
4039 other,
4040 "expected integer literal (signed)",
4041 )),
4042 }
4043}
4044
4045struct FieldInfo<'a> {
4046 ident: &'a syn::Ident,
4047 column: String,
4048 primary_key: bool,
4049 auto: bool,
4053 value_ty: &'a Type,
4056 field_type_tokens: TokenStream2,
4058 schema: TokenStream2,
4059 from_row_init: TokenStream2,
4060 from_aliased_row_init: TokenStream2,
4066 fk_inner: Option<Type>,
4070 fk_pk_kind: DetectedKind,
4076 nullable: bool,
4084 auto_now: bool,
4090 auto_now_add: bool,
4096 soft_delete: bool,
4101 generated_as: Option<String>,
4106}
4107
4108fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
4122 if name.is_empty() {
4123 return Err(syn::Error::new(
4124 span,
4125 "`table = \"\"` is not a valid SQL identifier",
4126 ));
4127 }
4128 let mut chars = name.chars();
4129 let first = chars.next().unwrap();
4130 if !(first.is_ascii_alphabetic() || first == '_') {
4131 return Err(syn::Error::new(
4132 span,
4133 format!("table name `{name}` must start with a letter or underscore (got {first:?})",),
4134 ));
4135 }
4136 for c in chars {
4137 if !(c.is_ascii_alphanumeric() || c == '_') {
4138 return Err(syn::Error::new(
4139 span,
4140 format!(
4141 "table name `{name}` contains invalid character {c:?} — \
4142 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
4143 Hyphens in particular break FK / index name derivation \
4144 downstream; use underscores instead (e.g. `{}`)",
4145 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
4146 ),
4147 ));
4148 }
4149 }
4150 Ok(())
4151}
4152
4153fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
4154 let attrs = parse_field_attrs(field)?;
4155 let ident = field
4156 .ident
4157 .as_ref()
4158 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4159 let name = ident.to_string();
4160 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
4161 let primary_key = attrs.primary_key;
4162 let DetectedType {
4163 kind,
4164 nullable,
4165 auto: detected_auto,
4166 fk_inner,
4167 } = detect_type(&field.ty)?;
4168 check_bound_compatibility(field, &attrs, kind)?;
4169 let auto = detected_auto;
4170 if attrs.auto_uuid {
4176 if kind != DetectedKind::Uuid {
4177 return Err(syn::Error::new_spanned(
4178 field,
4179 "`#[rustango(auto_uuid)]` requires the field type to be \
4180 `Auto<uuid::Uuid>`",
4181 ));
4182 }
4183 if !detected_auto {
4184 return Err(syn::Error::new_spanned(
4185 field,
4186 "`#[rustango(auto_uuid)]` requires the field type to be \
4187 wrapped in `Auto<...>` so the macro skips the column on \
4188 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
4189 ));
4190 }
4191 }
4192 if attrs.auto_now_add || attrs.auto_now {
4193 if kind != DetectedKind::DateTime {
4194 return Err(syn::Error::new_spanned(
4195 field,
4196 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4197 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
4198 ));
4199 }
4200 if !detected_auto {
4201 return Err(syn::Error::new_spanned(
4202 field,
4203 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4204 the field type to be wrapped in `Auto<...>` so the macro skips \
4205 the column on INSERT and the DB DEFAULT (`now()`) fires",
4206 ));
4207 }
4208 }
4209 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
4210 return Err(syn::Error::new_spanned(
4211 field,
4212 "`#[rustango(soft_delete)]` requires the field type to be \
4213 `Option<chrono::DateTime<chrono::Utc>>`",
4214 ));
4215 }
4216 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
4217 if detected_auto && !primary_key && !is_mixin_auto {
4218 return Err(syn::Error::new_spanned(
4219 field,
4220 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
4221 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
4222 `auto_now`",
4223 ));
4224 }
4225 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
4226 return Err(syn::Error::new_spanned(
4227 field,
4228 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
4229 SERIAL / BIGSERIAL already supplies a default sequence.",
4230 ));
4231 }
4232 if fk_inner.is_some() && primary_key {
4233 return Err(syn::Error::new_spanned(
4234 field,
4235 "`ForeignKey<T>` is not allowed on a primary-key field — \
4236 a row's PK is its own identity, not a reference to a parent.",
4237 ));
4238 }
4239 if attrs.generated_as.is_some() {
4240 if primary_key {
4241 return Err(syn::Error::new_spanned(
4242 field,
4243 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4244 primary-key field — a PK must be writable so the row \
4245 has an identity at INSERT time.",
4246 ));
4247 }
4248 if attrs.default.is_some() {
4249 return Err(syn::Error::new_spanned(
4250 field,
4251 "`#[rustango(generated_as = \"…\")]` cannot combine with \
4252 `default = \"…\"` — Postgres rejects DEFAULT on \
4253 generated columns. The expression IS the default.",
4254 ));
4255 }
4256 if detected_auto {
4257 return Err(syn::Error::new_spanned(
4258 field,
4259 "`#[rustango(generated_as = \"…\")]` is not allowed on \
4260 an `Auto<T>` field — generated columns are computed \
4261 by the DB, not server-assigned via a sequence. Use a \
4262 plain Rust type (e.g. `f64`).",
4263 ));
4264 }
4265 if fk_inner.is_some() {
4266 return Err(syn::Error::new_spanned(
4267 field,
4268 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4269 ForeignKey field.",
4270 ));
4271 }
4272 }
4273 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
4274 let column_lit = column.as_str();
4275 let field_type_tokens = kind.variant_tokens();
4276 let max_length = optional_u32(attrs.max_length);
4277 let min = optional_i64(attrs.min);
4278 let max = optional_i64(attrs.max);
4279 let default = optional_str(attrs.default.as_deref());
4280
4281 let unique = attrs.unique;
4282 let generated_as = optional_str(attrs.generated_as.as_deref());
4283 let schema = quote! {
4284 ::rustango::core::FieldSchema {
4285 name: #name,
4286 column: #column_lit,
4287 ty: #field_type_tokens,
4288 nullable: #nullable,
4289 primary_key: #primary_key,
4290 relation: #relation,
4291 max_length: #max_length,
4292 min: #min,
4293 max: #max,
4294 default: #default,
4295 auto: #auto,
4296 unique: #unique,
4297 generated_as: #generated_as,
4298 }
4299 };
4300
4301 let from_row_init = quote! {
4302 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
4303 };
4304 let from_aliased_row_init = quote! {
4305 #ident: ::rustango::sql::sqlx::Row::try_get(
4306 row,
4307 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
4308 )?
4309 };
4310
4311 Ok(FieldInfo {
4312 ident,
4313 column,
4314 primary_key,
4315 auto,
4316 value_ty: &field.ty,
4317 field_type_tokens,
4318 schema,
4319 from_row_init,
4320 from_aliased_row_init,
4321 fk_inner: fk_inner.cloned(),
4322 fk_pk_kind: kind,
4323 nullable,
4324 auto_now: attrs.auto_now,
4325 auto_now_add: attrs.auto_now_add,
4326 soft_delete: attrs.soft_delete,
4327 generated_as: attrs.generated_as.clone(),
4328 })
4329}
4330
4331fn check_bound_compatibility(
4332 field: &syn::Field,
4333 attrs: &FieldAttrs,
4334 kind: DetectedKind,
4335) -> syn::Result<()> {
4336 if attrs.max_length.is_some() && kind != DetectedKind::String {
4337 return Err(syn::Error::new_spanned(
4338 field,
4339 "`max_length` is only valid on `String` fields (or `Option<String>`)",
4340 ));
4341 }
4342 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
4343 return Err(syn::Error::new_spanned(
4344 field,
4345 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
4346 ));
4347 }
4348 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
4349 if min > max {
4350 return Err(syn::Error::new_spanned(
4351 field,
4352 format!("`min` ({min}) is greater than `max` ({max})"),
4353 ));
4354 }
4355 }
4356 Ok(())
4357}
4358
4359fn optional_u32(value: Option<u32>) -> TokenStream2 {
4360 if let Some(v) = value {
4361 quote!(::core::option::Option::Some(#v))
4362 } else {
4363 quote!(::core::option::Option::None)
4364 }
4365}
4366
4367fn optional_i64(value: Option<i64>) -> TokenStream2 {
4368 if let Some(v) = value {
4369 quote!(::core::option::Option::Some(#v))
4370 } else {
4371 quote!(::core::option::Option::None)
4372 }
4373}
4374
4375fn optional_str(value: Option<&str>) -> TokenStream2 {
4376 if let Some(v) = value {
4377 quote!(::core::option::Option::Some(#v))
4378 } else {
4379 quote!(::core::option::Option::None)
4380 }
4381}
4382
4383fn relation_tokens(
4384 field: &syn::Field,
4385 attrs: &FieldAttrs,
4386 fk_inner: Option<&syn::Type>,
4387 table: &str,
4388) -> syn::Result<TokenStream2> {
4389 if let Some(inner) = fk_inner {
4390 if attrs.fk.is_some() || attrs.o2o.is_some() {
4391 return Err(syn::Error::new_spanned(
4392 field,
4393 "`ForeignKey<T>` already declares the FK target via the type parameter — \
4394 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
4395 ));
4396 }
4397 let on = attrs.on.as_deref().unwrap_or("id");
4398 return Ok(quote! {
4399 ::core::option::Option::Some(::rustango::core::Relation::Fk {
4400 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
4401 on: #on,
4402 })
4403 });
4404 }
4405 match (&attrs.fk, &attrs.o2o) {
4406 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
4407 field,
4408 "`fk` and `o2o` are mutually exclusive",
4409 )),
4410 (Some(to), None) => {
4411 let on = attrs.on.as_deref().unwrap_or("id");
4412 let resolved = if to == "self" { table } else { to };
4418 Ok(quote! {
4419 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4420 })
4421 }
4422 (None, Some(to)) => {
4423 let on = attrs.on.as_deref().unwrap_or("id");
4424 let resolved = if to == "self" { table } else { to };
4425 Ok(quote! {
4426 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4427 })
4428 }
4429 (None, None) => {
4430 if attrs.on.is_some() {
4431 return Err(syn::Error::new_spanned(
4432 field,
4433 "`on` requires `fk` or `o2o`",
4434 ));
4435 }
4436 Ok(quote!(::core::option::Option::None))
4437 }
4438 }
4439}
4440
4441#[derive(Clone, Copy, PartialEq, Eq)]
4445enum DetectedKind {
4446 I16,
4447 I32,
4448 I64,
4449 F32,
4450 F64,
4451 Bool,
4452 String,
4453 DateTime,
4454 Date,
4455 Uuid,
4456 Json,
4457}
4458
4459impl DetectedKind {
4460 fn variant_tokens(self) -> TokenStream2 {
4461 match self {
4462 Self::I16 => quote!(::rustango::core::FieldType::I16),
4463 Self::I32 => quote!(::rustango::core::FieldType::I32),
4464 Self::I64 => quote!(::rustango::core::FieldType::I64),
4465 Self::F32 => quote!(::rustango::core::FieldType::F32),
4466 Self::F64 => quote!(::rustango::core::FieldType::F64),
4467 Self::Bool => quote!(::rustango::core::FieldType::Bool),
4468 Self::String => quote!(::rustango::core::FieldType::String),
4469 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4470 Self::Date => quote!(::rustango::core::FieldType::Date),
4471 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4472 Self::Json => quote!(::rustango::core::FieldType::Json),
4473 }
4474 }
4475
4476 fn is_integer(self) -> bool {
4477 matches!(self, Self::I16 | Self::I32 | Self::I64)
4478 }
4479
4480 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
4488 match self {
4489 Self::I16 => (quote!(I16), quote!(0i16)),
4490 Self::I32 => (quote!(I32), quote!(0i32)),
4491 Self::I64 => (quote!(I64), quote!(0i64)),
4492 Self::F32 => (quote!(F32), quote!(0f32)),
4493 Self::F64 => (quote!(F64), quote!(0f64)),
4494 Self::Bool => (quote!(Bool), quote!(false)),
4495 Self::String => (quote!(String), quote!(::std::string::String::new())),
4496 Self::DateTime => (
4497 quote!(DateTime),
4498 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
4499 ),
4500 Self::Date => (
4501 quote!(Date),
4502 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
4503 ),
4504 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
4505 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
4506 }
4507 }
4508}
4509
4510#[derive(Clone, Copy)]
4516struct DetectedType<'a> {
4517 kind: DetectedKind,
4518 nullable: bool,
4519 auto: bool,
4520 fk_inner: Option<&'a syn::Type>,
4521}
4522
4523fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4528 let Type::Path(TypePath { path, qself: None }) = ty else {
4529 return None;
4530 };
4531 let last = path.segments.last()?;
4532 if last.ident != "Auto" {
4533 return None;
4534 }
4535 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4536 return None;
4537 };
4538 args.args.iter().find_map(|a| match a {
4539 syn::GenericArgument::Type(t) => Some(t),
4540 _ => None,
4541 })
4542}
4543
4544fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4545 let Type::Path(TypePath { path, qself: None }) = ty else {
4546 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4547 };
4548 let last = path
4549 .segments
4550 .last()
4551 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4552
4553 if last.ident == "Option" {
4554 let inner = generic_inner(ty, &last.arguments, "Option")?;
4555 let inner_det = detect_type(inner)?;
4556 if inner_det.nullable {
4557 return Err(syn::Error::new_spanned(
4558 ty,
4559 "nested Option is not supported",
4560 ));
4561 }
4562 if inner_det.auto {
4563 return Err(syn::Error::new_spanned(
4564 ty,
4565 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4566 ));
4567 }
4568 return Ok(DetectedType {
4569 nullable: true,
4570 ..inner_det
4571 });
4572 }
4573
4574 if last.ident == "Auto" {
4575 let inner = generic_inner(ty, &last.arguments, "Auto")?;
4576 let inner_det = detect_type(inner)?;
4577 if inner_det.auto {
4578 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
4579 }
4580 if inner_det.nullable {
4581 return Err(syn::Error::new_spanned(
4582 ty,
4583 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4584 ));
4585 }
4586 if inner_det.fk_inner.is_some() {
4587 return Err(syn::Error::new_spanned(
4588 ty,
4589 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4590 ));
4591 }
4592 if !matches!(
4593 inner_det.kind,
4594 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4595 ) {
4596 return Err(syn::Error::new_spanned(
4597 ty,
4598 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4599 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4600 (DEFAULT now())",
4601 ));
4602 }
4603 return Ok(DetectedType {
4604 auto: true,
4605 ..inner_det
4606 });
4607 }
4608
4609 if last.ident == "ForeignKey" {
4610 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
4611 let kind = match key_ty {
4619 Some(k) => detect_type(k)?.kind,
4620 None => DetectedKind::I64,
4621 };
4622 return Ok(DetectedType {
4623 kind,
4624 nullable: false,
4625 auto: false,
4626 fk_inner: Some(inner),
4627 });
4628 }
4629
4630 let kind = match last.ident.to_string().as_str() {
4631 "i16" => DetectedKind::I16,
4632 "i32" => DetectedKind::I32,
4633 "i64" => DetectedKind::I64,
4634 "f32" => DetectedKind::F32,
4635 "f64" => DetectedKind::F64,
4636 "bool" => DetectedKind::Bool,
4637 "String" => DetectedKind::String,
4638 "DateTime" => DetectedKind::DateTime,
4639 "NaiveDate" => DetectedKind::Date,
4640 "Uuid" => DetectedKind::Uuid,
4641 "Value" => DetectedKind::Json,
4642 other => {
4643 return Err(syn::Error::new_spanned(
4644 ty,
4645 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)"),
4646 ));
4647 }
4648 };
4649 Ok(DetectedType {
4650 kind,
4651 nullable: false,
4652 auto: false,
4653 fk_inner: None,
4654 })
4655}
4656
4657fn generic_inner<'a>(
4658 ty: &'a Type,
4659 arguments: &'a PathArguments,
4660 wrapper: &str,
4661) -> syn::Result<&'a Type> {
4662 let PathArguments::AngleBracketed(args) = arguments else {
4663 return Err(syn::Error::new_spanned(
4664 ty,
4665 format!("{wrapper} requires a generic argument"),
4666 ));
4667 };
4668 args.args
4669 .iter()
4670 .find_map(|a| match a {
4671 GenericArgument::Type(t) => Some(t),
4672 _ => None,
4673 })
4674 .ok_or_else(|| {
4675 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4676 })
4677}
4678
4679fn generic_pair<'a>(
4683 ty: &'a Type,
4684 arguments: &'a PathArguments,
4685 wrapper: &str,
4686) -> syn::Result<(&'a Type, Option<&'a Type>)> {
4687 let PathArguments::AngleBracketed(args) = arguments else {
4688 return Err(syn::Error::new_spanned(
4689 ty,
4690 format!("{wrapper} requires a generic argument"),
4691 ));
4692 };
4693 let mut types = args.args.iter().filter_map(|a| match a {
4694 GenericArgument::Type(t) => Some(t),
4695 _ => None,
4696 });
4697 let first = types.next().ok_or_else(|| {
4698 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4699 })?;
4700 let second = types.next();
4701 Ok((first, second))
4702}
4703
4704fn to_snake_case(s: &str) -> String {
4705 let mut out = String::with_capacity(s.len() + 4);
4706 for (i, ch) in s.chars().enumerate() {
4707 if ch.is_ascii_uppercase() {
4708 if i > 0 {
4709 out.push('_');
4710 }
4711 out.push(ch.to_ascii_lowercase());
4712 } else {
4713 out.push(ch);
4714 }
4715 }
4716 out
4717}
4718
4719#[derive(Default)]
4725struct FormFieldAttrs {
4726 min: Option<i64>,
4727 max: Option<i64>,
4728 min_length: Option<u32>,
4729 max_length: Option<u32>,
4730}
4731
4732#[derive(Clone, Copy)]
4734enum FormFieldKind {
4735 String,
4736 I16,
4737 I32,
4738 I64,
4739 F32,
4740 F64,
4741 Bool,
4742}
4743
4744impl FormFieldKind {
4745 fn parse_method(self) -> &'static str {
4746 match self {
4747 Self::I16 => "i16",
4748 Self::I32 => "i32",
4749 Self::I64 => "i64",
4750 Self::F32 => "f32",
4751 Self::F64 => "f64",
4752 Self::String | Self::Bool => "",
4755 }
4756 }
4757}
4758
4759fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4760 let struct_name = &input.ident;
4761
4762 let Data::Struct(data) = &input.data else {
4763 return Err(syn::Error::new_spanned(
4764 struct_name,
4765 "Form can only be derived on structs",
4766 ));
4767 };
4768 let Fields::Named(named) = &data.fields else {
4769 return Err(syn::Error::new_spanned(
4770 struct_name,
4771 "Form requires a struct with named fields",
4772 ));
4773 };
4774
4775 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4776 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4777
4778 for field in &named.named {
4779 let ident = field
4780 .ident
4781 .as_ref()
4782 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4783 let attrs = parse_form_field_attrs(field)?;
4784 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4785
4786 let name_lit = ident.to_string();
4787 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4788 field_blocks.push(parse_block);
4789 field_idents.push(ident);
4790 }
4791
4792 Ok(quote! {
4793 impl ::rustango::forms::Form for #struct_name {
4794 fn parse(
4795 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4796 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4797 let mut __errors = ::rustango::forms::FormErrors::default();
4798 #( #field_blocks )*
4799 if !__errors.is_empty() {
4800 return ::core::result::Result::Err(__errors);
4801 }
4802 ::core::result::Result::Ok(Self {
4803 #( #field_idents ),*
4804 })
4805 }
4806 }
4807 })
4808}
4809
4810fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4811 let mut out = FormFieldAttrs::default();
4812 for attr in &field.attrs {
4813 if !attr.path().is_ident("form") {
4814 continue;
4815 }
4816 attr.parse_nested_meta(|meta| {
4817 if meta.path.is_ident("min") {
4818 let lit: syn::LitInt = meta.value()?.parse()?;
4819 out.min = Some(lit.base10_parse::<i64>()?);
4820 return Ok(());
4821 }
4822 if meta.path.is_ident("max") {
4823 let lit: syn::LitInt = meta.value()?.parse()?;
4824 out.max = Some(lit.base10_parse::<i64>()?);
4825 return Ok(());
4826 }
4827 if meta.path.is_ident("min_length") {
4828 let lit: syn::LitInt = meta.value()?.parse()?;
4829 out.min_length = Some(lit.base10_parse::<u32>()?);
4830 return Ok(());
4831 }
4832 if meta.path.is_ident("max_length") {
4833 let lit: syn::LitInt = meta.value()?.parse()?;
4834 out.max_length = Some(lit.base10_parse::<u32>()?);
4835 return Ok(());
4836 }
4837 Err(meta.error(
4838 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4839 ))
4840 })?;
4841 }
4842 Ok(out)
4843}
4844
4845fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4846 let Type::Path(TypePath { path, qself: None }) = ty else {
4847 return Err(syn::Error::new(
4848 span,
4849 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4850 ));
4851 };
4852 let last = path
4853 .segments
4854 .last()
4855 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4856
4857 if last.ident == "Option" {
4858 let inner = generic_inner(ty, &last.arguments, "Option")?;
4859 let (kind, nested) = detect_form_field(inner, span)?;
4860 if nested {
4861 return Err(syn::Error::new(
4862 span,
4863 "nested Option in Form fields is not supported",
4864 ));
4865 }
4866 return Ok((kind, true));
4867 }
4868
4869 let kind = match last.ident.to_string().as_str() {
4870 "String" => FormFieldKind::String,
4871 "i16" => FormFieldKind::I16,
4872 "i32" => FormFieldKind::I32,
4873 "i64" => FormFieldKind::I64,
4874 "f32" => FormFieldKind::F32,
4875 "f64" => FormFieldKind::F64,
4876 "bool" => FormFieldKind::Bool,
4877 other => {
4878 return Err(syn::Error::new(
4879 span,
4880 format!(
4881 "Form field type `{other}` is not supported in v0.8 — use String / \
4882 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4883 ),
4884 ));
4885 }
4886 };
4887 Ok((kind, false))
4888}
4889
4890#[allow(clippy::too_many_lines)]
4891fn render_form_field_parse(
4892 ident: &syn::Ident,
4893 name_lit: &str,
4894 kind: FormFieldKind,
4895 nullable: bool,
4896 attrs: &FormFieldAttrs,
4897) -> TokenStream2 {
4898 let lookup = quote! {
4901 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4902 };
4903
4904 let parsed_value = match kind {
4905 FormFieldKind::Bool => quote! {
4906 let __v: bool = match __raw {
4907 ::core::option::Option::None => false,
4908 ::core::option::Option::Some(__s) => !matches!(
4909 __s.to_ascii_lowercase().as_str(),
4910 "" | "false" | "0" | "off" | "no"
4911 ),
4912 };
4913 },
4914 FormFieldKind::String => {
4915 if nullable {
4916 quote! {
4917 let __v: ::core::option::Option<::std::string::String> = match __raw {
4918 ::core::option::Option::None => ::core::option::Option::None,
4919 ::core::option::Option::Some(__s) if __s.is_empty() => {
4920 ::core::option::Option::None
4921 }
4922 ::core::option::Option::Some(__s) => {
4923 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4924 }
4925 };
4926 }
4927 } else {
4928 quote! {
4929 let __v: ::std::string::String = match __raw {
4930 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4931 ::core::clone::Clone::clone(__s)
4932 }
4933 _ => {
4934 __errors.add(#name_lit, "This field is required.");
4935 ::std::string::String::new()
4936 }
4937 };
4938 }
4939 }
4940 }
4941 FormFieldKind::I16
4942 | FormFieldKind::I32
4943 | FormFieldKind::I64
4944 | FormFieldKind::F32
4945 | FormFieldKind::F64 => {
4946 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
4947 let ty_lit = kind.parse_method();
4948 let default_val = match kind {
4949 FormFieldKind::I16 => quote! { 0i16 },
4950 FormFieldKind::I32 => quote! { 0i32 },
4951 FormFieldKind::I64 => quote! { 0i64 },
4952 FormFieldKind::F32 => quote! { 0f32 },
4953 FormFieldKind::F64 => quote! { 0f64 },
4954 _ => quote! { Default::default() },
4955 };
4956 if nullable {
4957 quote! {
4958 let __v: ::core::option::Option<#parse_ty> = match __raw {
4959 ::core::option::Option::None => ::core::option::Option::None,
4960 ::core::option::Option::Some(__s) if __s.is_empty() => {
4961 ::core::option::Option::None
4962 }
4963 ::core::option::Option::Some(__s) => {
4964 match __s.parse::<#parse_ty>() {
4965 ::core::result::Result::Ok(__n) => {
4966 ::core::option::Option::Some(__n)
4967 }
4968 ::core::result::Result::Err(__e) => {
4969 __errors.add(
4970 #name_lit,
4971 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4972 );
4973 ::core::option::Option::None
4974 }
4975 }
4976 }
4977 };
4978 }
4979 } else {
4980 quote! {
4981 let __v: #parse_ty = match __raw {
4982 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4983 match __s.parse::<#parse_ty>() {
4984 ::core::result::Result::Ok(__n) => __n,
4985 ::core::result::Result::Err(__e) => {
4986 __errors.add(
4987 #name_lit,
4988 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4989 );
4990 #default_val
4991 }
4992 }
4993 }
4994 _ => {
4995 __errors.add(#name_lit, "This field is required.");
4996 #default_val
4997 }
4998 };
4999 }
5000 }
5001 }
5002 };
5003
5004 let validators = render_form_validators(name_lit, kind, nullable, attrs);
5005
5006 quote! {
5007 let #ident = {
5008 #lookup
5009 #parsed_value
5010 #validators
5011 __v
5012 };
5013 }
5014}
5015
5016fn render_form_validators(
5017 name_lit: &str,
5018 kind: FormFieldKind,
5019 nullable: bool,
5020 attrs: &FormFieldAttrs,
5021) -> TokenStream2 {
5022 let mut checks: Vec<TokenStream2> = Vec::new();
5023
5024 let val_ref = if nullable {
5025 quote! { __v.as_ref() }
5026 } else {
5027 quote! { ::core::option::Option::Some(&__v) }
5028 };
5029
5030 let is_string = matches!(kind, FormFieldKind::String);
5031 let is_numeric = matches!(
5032 kind,
5033 FormFieldKind::I16
5034 | FormFieldKind::I32
5035 | FormFieldKind::I64
5036 | FormFieldKind::F32
5037 | FormFieldKind::F64
5038 );
5039
5040 if is_string {
5041 if let Some(min_len) = attrs.min_length {
5042 let min_len_usize = min_len as usize;
5043 checks.push(quote! {
5044 if let ::core::option::Option::Some(__s) = #val_ref {
5045 if __s.len() < #min_len_usize {
5046 __errors.add(
5047 #name_lit,
5048 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
5049 );
5050 }
5051 }
5052 });
5053 }
5054 if let Some(max_len) = attrs.max_length {
5055 let max_len_usize = max_len as usize;
5056 checks.push(quote! {
5057 if let ::core::option::Option::Some(__s) = #val_ref {
5058 if __s.len() > #max_len_usize {
5059 __errors.add(
5060 #name_lit,
5061 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
5062 );
5063 }
5064 }
5065 });
5066 }
5067 }
5068
5069 if is_numeric {
5070 if let Some(min) = attrs.min {
5071 checks.push(quote! {
5072 if let ::core::option::Option::Some(__n) = #val_ref {
5073 if (*__n as f64) < (#min as f64) {
5074 __errors.add(
5075 #name_lit,
5076 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
5077 );
5078 }
5079 }
5080 });
5081 }
5082 if let Some(max) = attrs.max {
5083 checks.push(quote! {
5084 if let ::core::option::Option::Some(__n) = #val_ref {
5085 if (*__n as f64) > (#max as f64) {
5086 __errors.add(
5087 #name_lit,
5088 ::std::format!("Ensure this value is less than or equal to {}.", #max),
5089 );
5090 }
5091 }
5092 });
5093 }
5094 }
5095
5096 quote! { #( #checks )* }
5097}
5098
5099struct ViewSetAttrs {
5104 model: syn::Path,
5105 fields: Option<Vec<String>>,
5106 filter_fields: Vec<String>,
5107 search_fields: Vec<String>,
5108 ordering: Vec<(String, bool)>,
5110 page_size: Option<usize>,
5111 read_only: bool,
5112 perms: ViewSetPermsAttrs,
5113}
5114
5115#[derive(Default)]
5116struct ViewSetPermsAttrs {
5117 list: Vec<String>,
5118 retrieve: Vec<String>,
5119 create: Vec<String>,
5120 update: Vec<String>,
5121 destroy: Vec<String>,
5122}
5123
5124fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
5125 let struct_name = &input.ident;
5126
5127 match &input.data {
5129 Data::Struct(s) => match &s.fields {
5130 Fields::Unit | Fields::Named(_) => {}
5131 Fields::Unnamed(_) => {
5132 return Err(syn::Error::new_spanned(
5133 struct_name,
5134 "ViewSet can only be derived on a unit struct or an empty named struct",
5135 ));
5136 }
5137 },
5138 _ => {
5139 return Err(syn::Error::new_spanned(
5140 struct_name,
5141 "ViewSet can only be derived on a struct",
5142 ));
5143 }
5144 }
5145
5146 let attrs = parse_viewset_attrs(input)?;
5147 let model_path = &attrs.model;
5148
5149 let fields_call = if let Some(ref fields) = attrs.fields {
5151 let lits = fields.iter().map(|f| f.as_str());
5152 quote!(.fields(&[ #(#lits),* ]))
5153 } else {
5154 quote!()
5155 };
5156
5157 let filter_fields_call = if attrs.filter_fields.is_empty() {
5158 quote!()
5159 } else {
5160 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
5161 quote!(.filter_fields(&[ #(#lits),* ]))
5162 };
5163
5164 let search_fields_call = if attrs.search_fields.is_empty() {
5165 quote!()
5166 } else {
5167 let lits = attrs.search_fields.iter().map(|f| f.as_str());
5168 quote!(.search_fields(&[ #(#lits),* ]))
5169 };
5170
5171 let ordering_call = if attrs.ordering.is_empty() {
5172 quote!()
5173 } else {
5174 let pairs = attrs.ordering.iter().map(|(f, desc)| {
5175 let f = f.as_str();
5176 quote!((#f, #desc))
5177 });
5178 quote!(.ordering(&[ #(#pairs),* ]))
5179 };
5180
5181 let page_size_call = if let Some(n) = attrs.page_size {
5182 quote!(.page_size(#n))
5183 } else {
5184 quote!()
5185 };
5186
5187 let read_only_call = if attrs.read_only {
5188 quote!(.read_only())
5189 } else {
5190 quote!()
5191 };
5192
5193 let perms = &attrs.perms;
5194 let perms_call = if perms.list.is_empty()
5195 && perms.retrieve.is_empty()
5196 && perms.create.is_empty()
5197 && perms.update.is_empty()
5198 && perms.destroy.is_empty()
5199 {
5200 quote!()
5201 } else {
5202 let list_lits = perms.list.iter().map(|s| s.as_str());
5203 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
5204 let create_lits = perms.create.iter().map(|s| s.as_str());
5205 let update_lits = perms.update.iter().map(|s| s.as_str());
5206 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
5207 quote! {
5208 .permissions(::rustango::viewset::ViewSetPerms {
5209 list: ::std::vec![ #(#list_lits.to_owned()),* ],
5210 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
5211 create: ::std::vec![ #(#create_lits.to_owned()),* ],
5212 update: ::std::vec![ #(#update_lits.to_owned()),* ],
5213 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
5214 })
5215 }
5216 };
5217
5218 Ok(quote! {
5219 impl #struct_name {
5220 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
5223 ::rustango::viewset::ViewSet::for_model(
5224 <#model_path as ::rustango::core::Model>::SCHEMA
5225 )
5226 #fields_call
5227 #filter_fields_call
5228 #search_fields_call
5229 #ordering_call
5230 #page_size_call
5231 #perms_call
5232 #read_only_call
5233 .router(prefix, pool)
5234 }
5235 }
5236 })
5237}
5238
5239fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
5240 let mut model: Option<syn::Path> = None;
5241 let mut fields: Option<Vec<String>> = None;
5242 let mut filter_fields: Vec<String> = Vec::new();
5243 let mut search_fields: Vec<String> = Vec::new();
5244 let mut ordering: Vec<(String, bool)> = Vec::new();
5245 let mut page_size: Option<usize> = None;
5246 let mut read_only = false;
5247 let mut perms = ViewSetPermsAttrs::default();
5248
5249 for attr in &input.attrs {
5250 if !attr.path().is_ident("viewset") {
5251 continue;
5252 }
5253 attr.parse_nested_meta(|meta| {
5254 if meta.path.is_ident("model") {
5255 let path: syn::Path = meta.value()?.parse()?;
5256 model = Some(path);
5257 return Ok(());
5258 }
5259 if meta.path.is_ident("fields") {
5260 let s: LitStr = meta.value()?.parse()?;
5261 fields = Some(split_field_list(&s.value()));
5262 return Ok(());
5263 }
5264 if meta.path.is_ident("filter_fields") {
5265 let s: LitStr = meta.value()?.parse()?;
5266 filter_fields = split_field_list(&s.value());
5267 return Ok(());
5268 }
5269 if meta.path.is_ident("search_fields") {
5270 let s: LitStr = meta.value()?.parse()?;
5271 search_fields = split_field_list(&s.value());
5272 return Ok(());
5273 }
5274 if meta.path.is_ident("ordering") {
5275 let s: LitStr = meta.value()?.parse()?;
5276 ordering = parse_ordering_list(&s.value());
5277 return Ok(());
5278 }
5279 if meta.path.is_ident("page_size") {
5280 let lit: syn::LitInt = meta.value()?.parse()?;
5281 page_size = Some(lit.base10_parse::<usize>()?);
5282 return Ok(());
5283 }
5284 if meta.path.is_ident("read_only") {
5285 read_only = true;
5286 return Ok(());
5287 }
5288 if meta.path.is_ident("permissions") {
5289 meta.parse_nested_meta(|inner| {
5290 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
5291 let s: LitStr = inner.value()?.parse()?;
5292 Ok(split_field_list(&s.value()))
5293 };
5294 if inner.path.is_ident("list") {
5295 perms.list = parse_codenames(&inner)?;
5296 } else if inner.path.is_ident("retrieve") {
5297 perms.retrieve = parse_codenames(&inner)?;
5298 } else if inner.path.is_ident("create") {
5299 perms.create = parse_codenames(&inner)?;
5300 } else if inner.path.is_ident("update") {
5301 perms.update = parse_codenames(&inner)?;
5302 } else if inner.path.is_ident("destroy") {
5303 perms.destroy = parse_codenames(&inner)?;
5304 } else {
5305 return Err(inner.error(
5306 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
5307 ));
5308 }
5309 Ok(())
5310 })?;
5311 return Ok(());
5312 }
5313 Err(meta.error(
5314 "unknown viewset attribute (supported: model, fields, filter_fields, \
5315 search_fields, ordering, page_size, read_only, permissions(...))",
5316 ))
5317 })?;
5318 }
5319
5320 let model = model.ok_or_else(|| {
5321 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
5322 })?;
5323
5324 Ok(ViewSetAttrs {
5325 model,
5326 fields,
5327 filter_fields,
5328 search_fields,
5329 ordering,
5330 page_size,
5331 read_only,
5332 perms,
5333 })
5334}
5335
5336struct SerializerContainerAttrs {
5339 model: syn::Path,
5340}
5341
5342#[derive(Default)]
5343struct SerializerFieldAttrs {
5344 read_only: bool,
5345 write_only: bool,
5346 source: Option<String>,
5347 skip: bool,
5348 method: Option<String>,
5352 validate: Option<String>,
5357 nested: bool,
5367 nested_strict: bool,
5372 many: Option<syn::Type>,
5381}
5382
5383fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
5384 let mut model: Option<syn::Path> = None;
5385 for attr in &input.attrs {
5386 if !attr.path().is_ident("serializer") {
5387 continue;
5388 }
5389 attr.parse_nested_meta(|meta| {
5390 if meta.path.is_ident("model") {
5391 let _eq: syn::Token![=] = meta.input.parse()?;
5392 model = Some(meta.input.parse()?);
5393 return Ok(());
5394 }
5395 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
5396 })?;
5397 }
5398 let model = model.ok_or_else(|| {
5399 syn::Error::new_spanned(
5400 &input.ident,
5401 "`#[serializer(model = SomeModel)]` is required",
5402 )
5403 })?;
5404 Ok(SerializerContainerAttrs { model })
5405}
5406
5407fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
5408 let mut out = SerializerFieldAttrs::default();
5409 for attr in &field.attrs {
5410 if !attr.path().is_ident("serializer") {
5411 continue;
5412 }
5413 attr.parse_nested_meta(|meta| {
5414 if meta.path.is_ident("read_only") {
5415 out.read_only = true;
5416 return Ok(());
5417 }
5418 if meta.path.is_ident("write_only") {
5419 out.write_only = true;
5420 return Ok(());
5421 }
5422 if meta.path.is_ident("skip") {
5423 out.skip = true;
5424 return Ok(());
5425 }
5426 if meta.path.is_ident("source") {
5427 let s: LitStr = meta.value()?.parse()?;
5428 out.source = Some(s.value());
5429 return Ok(());
5430 }
5431 if meta.path.is_ident("method") {
5432 let s: LitStr = meta.value()?.parse()?;
5433 out.method = Some(s.value());
5434 return Ok(());
5435 }
5436 if meta.path.is_ident("validate") {
5437 let s: LitStr = meta.value()?.parse()?;
5438 out.validate = Some(s.value());
5439 return Ok(());
5440 }
5441 if meta.path.is_ident("many") {
5442 let _eq: syn::Token![=] = meta.input.parse()?;
5443 out.many = Some(meta.input.parse()?);
5444 return Ok(());
5445 }
5446 if meta.path.is_ident("nested") {
5447 out.nested = true;
5448 if meta.input.peek(syn::token::Paren) {
5451 meta.parse_nested_meta(|inner| {
5452 if inner.path.is_ident("strict") {
5453 out.nested_strict = true;
5454 return Ok(());
5455 }
5456 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
5457 })?;
5458 }
5459 return Ok(());
5460 }
5461 Err(meta.error(
5462 "unknown serializer field attribute (supported: \
5463 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
5464 ))
5465 })?;
5466 }
5467 if out.read_only && out.write_only {
5469 return Err(syn::Error::new_spanned(
5470 field,
5471 "a field cannot be both `read_only` and `write_only`",
5472 ));
5473 }
5474 if out.method.is_some() && out.source.is_some() {
5475 return Err(syn::Error::new_spanned(
5476 field,
5477 "`method` and `source` are mutually exclusive — `method` computes \
5478 the value from a method, `source` reads it from a different model field",
5479 ));
5480 }
5481 Ok(out)
5482}
5483
5484fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
5485 let struct_name = &input.ident;
5486 let struct_name_lit = struct_name.to_string();
5487
5488 let Data::Struct(data) = &input.data else {
5489 return Err(syn::Error::new_spanned(
5490 struct_name,
5491 "Serializer can only be derived on structs",
5492 ));
5493 };
5494 let Fields::Named(named) = &data.fields else {
5495 return Err(syn::Error::new_spanned(
5496 struct_name,
5497 "Serializer requires a struct with named fields",
5498 ));
5499 };
5500
5501 let container = parse_serializer_container_attrs(input)?;
5502 let model_path = &container.model;
5503
5504 #[allow(dead_code)]
5508 struct FieldInfo {
5509 ident: syn::Ident,
5510 ty: syn::Type,
5511 attrs: SerializerFieldAttrs,
5512 }
5513 let mut fields_info: Vec<FieldInfo> = Vec::new();
5514 for field in &named.named {
5515 let ident = field.ident.clone().expect("named field has ident");
5516 let attrs = parse_serializer_field_attrs(field)?;
5517 fields_info.push(FieldInfo {
5518 ident,
5519 ty: field.ty.clone(),
5520 attrs,
5521 });
5522 }
5523
5524 let from_model_fields = fields_info.iter().map(|fi| {
5526 let ident = &fi.ident;
5527 let ty = &fi.ty;
5528 if let Some(_inner) = &fi.attrs.many {
5529 quote! { #ident: ::std::vec::Vec::new() }
5533 } else if let Some(method) = &fi.attrs.method {
5534 let method_ident = syn::Ident::new(method, ident.span());
5538 quote! { #ident: Self::#method_ident(model) }
5539 } else if fi.attrs.nested {
5540 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5556 let src_ident = syn::Ident::new(&src_name, ident.span());
5557 if fi.attrs.nested_strict {
5558 let panic_msg = format!(
5559 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5560 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5561 );
5562 quote! {
5563 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5564 model.#src_ident.value().expect(#panic_msg),
5565 )
5566 }
5567 } else {
5568 quote! {
5569 #ident: match model.#src_ident.value() {
5570 ::core::option::Option::Some(__loaded) =>
5571 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5572 ::core::option::Option::None =>
5573 ::core::default::Default::default(),
5574 }
5575 }
5576 }
5577 } else if fi.attrs.write_only || fi.attrs.skip {
5578 quote! { #ident: ::core::default::Default::default() }
5580 } else if let Some(src) = &fi.attrs.source {
5581 let src_ident = syn::Ident::new(src, ident.span());
5582 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5583 } else {
5584 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5585 }
5586 });
5587
5588 let validator_calls: Vec<_> = fields_info
5592 .iter()
5593 .filter_map(|fi| {
5594 let ident = &fi.ident;
5595 let name_lit = ident.to_string();
5596 let method = fi.attrs.validate.as_ref()?;
5597 let method_ident = syn::Ident::new(method, ident.span());
5598 Some(quote! {
5599 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5600 __errors.add(#name_lit.to_owned(), __e);
5601 }
5602 })
5603 })
5604 .collect();
5605 let validate_method = if validator_calls.is_empty() {
5606 quote! {}
5607 } else {
5608 quote! {
5609 impl #struct_name {
5610 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5614 let mut __errors = ::rustango::forms::FormErrors::default();
5615 #( #validator_calls )*
5616 if __errors.is_empty() {
5617 ::core::result::Result::Ok(())
5618 } else {
5619 ::core::result::Result::Err(__errors)
5620 }
5621 }
5622 }
5623 }
5624 };
5625
5626 let many_setters: Vec<_> = fields_info
5630 .iter()
5631 .filter_map(|fi| {
5632 let many_ty = fi.attrs.many.as_ref()?;
5633 let ident = &fi.ident;
5634 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5635 Some(quote! {
5636 pub fn #setter(
5641 &mut self,
5642 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5643 ) -> &mut Self {
5644 self.#ident = models.iter()
5645 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5646 .collect();
5647 self
5648 }
5649 })
5650 })
5651 .collect();
5652 let many_setters_impl = if many_setters.is_empty() {
5653 quote! {}
5654 } else {
5655 quote! {
5656 impl #struct_name {
5657 #( #many_setters )*
5658 }
5659 }
5660 };
5661
5662 let output_fields: Vec<_> = fields_info
5664 .iter()
5665 .filter(|fi| !fi.attrs.write_only)
5666 .collect();
5667 let output_field_count = output_fields.len();
5668 let serialize_fields = output_fields.iter().map(|fi| {
5669 let ident = &fi.ident;
5670 let name_lit = ident.to_string();
5671 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5672 });
5673
5674 let writable_lits: Vec<_> = fields_info
5676 .iter()
5677 .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5678 .map(|fi| fi.ident.to_string())
5679 .collect();
5680
5681 let openapi_impl = {
5685 #[cfg(feature = "openapi")]
5686 {
5687 let property_calls = output_fields.iter().map(|fi| {
5688 let ident = &fi.ident;
5689 let name_lit = ident.to_string();
5690 let ty = &fi.ty;
5691 let nullable_call = if is_option(ty) {
5692 quote! { .nullable() }
5693 } else {
5694 quote! {}
5695 };
5696 quote! {
5697 .property(
5698 #name_lit,
5699 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5700 #nullable_call,
5701 )
5702 }
5703 });
5704 let required_lits: Vec<_> = output_fields
5705 .iter()
5706 .filter(|fi| !is_option(&fi.ty))
5707 .map(|fi| fi.ident.to_string())
5708 .collect();
5709 quote! {
5710 impl ::rustango::openapi::OpenApiSchema for #struct_name {
5711 fn openapi_schema() -> ::rustango::openapi::Schema {
5712 ::rustango::openapi::Schema::object()
5713 #( #property_calls )*
5714 .required([ #( #required_lits ),* ])
5715 }
5716 }
5717 }
5718 }
5719 #[cfg(not(feature = "openapi"))]
5720 {
5721 quote! {}
5722 }
5723 };
5724
5725 Ok(quote! {
5726 impl ::rustango::serializer::ModelSerializer for #struct_name {
5727 type Model = #model_path;
5728
5729 fn from_model(model: &Self::Model) -> Self {
5730 Self {
5731 #( #from_model_fields ),*
5732 }
5733 }
5734
5735 fn writable_fields() -> &'static [&'static str] {
5736 &[ #( #writable_lits ),* ]
5737 }
5738 }
5739
5740 impl ::serde::Serialize for #struct_name {
5741 fn serialize<S>(&self, serializer: S)
5742 -> ::core::result::Result<S::Ok, S::Error>
5743 where
5744 S: ::serde::Serializer,
5745 {
5746 use ::serde::ser::SerializeStruct;
5747 let mut __state = serializer.serialize_struct(
5748 #struct_name_lit,
5749 #output_field_count,
5750 )?;
5751 #( #serialize_fields )*
5752 __state.end()
5753 }
5754 }
5755
5756 #openapi_impl
5757
5758 #validate_method
5759
5760 #many_setters_impl
5761 })
5762}
5763
5764#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5768fn is_option(ty: &syn::Type) -> bool {
5769 if let syn::Type::Path(p) = ty {
5770 if let Some(last) = p.path.segments.last() {
5771 return last.ident == "Option";
5772 }
5773 }
5774 false
5775}