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