1use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17#[proc_macro_derive(Model, attributes(rustango))]
19pub fn derive_model(input: TokenStream) -> TokenStream {
20 let input = parse_macro_input!(input as DeriveInput);
21 expand(&input)
22 .unwrap_or_else(syn::Error::into_compile_error)
23 .into()
24}
25
26#[proc_macro_derive(ViewSet, attributes(viewset))]
59pub fn derive_viewset(input: TokenStream) -> TokenStream {
60 let input = parse_macro_input!(input as DeriveInput);
61 expand_viewset(&input)
62 .unwrap_or_else(syn::Error::into_compile_error)
63 .into()
64}
65
66#[proc_macro_derive(Form, attributes(form))]
94pub fn derive_form(input: TokenStream) -> TokenStream {
95 let input = parse_macro_input!(input as DeriveInput);
96 expand_form(&input)
97 .unwrap_or_else(syn::Error::into_compile_error)
98 .into()
99}
100
101#[proc_macro_derive(Serializer, attributes(serializer))]
114pub fn derive_serializer(input: TokenStream) -> TokenStream {
115 let input = parse_macro_input!(input as DeriveInput);
116 expand_serializer(&input)
117 .unwrap_or_else(syn::Error::into_compile_error)
118 .into()
119}
120
121#[proc_macro]
156pub fn embed_migrations(input: TokenStream) -> TokenStream {
157 expand_embed_migrations(input.into())
158 .unwrap_or_else(syn::Error::into_compile_error)
159 .into()
160}
161
162#[proc_macro_attribute]
185pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
186 expand_main(args.into(), item.into())
187 .unwrap_or_else(syn::Error::into_compile_error)
188 .into()
189}
190
191fn expand_main(
192 args: TokenStream2,
193 item: TokenStream2,
194) -> syn::Result<TokenStream2> {
195 let mut input: syn::ItemFn = syn::parse2(item)?;
196 if input.sig.asyncness.is_none() {
197 return Err(syn::Error::new(
198 input.sig.ident.span(),
199 "`#[rustango::main]` must wrap an `async fn`",
200 ));
201 }
202
203 let tokio_attr = if args.is_empty() {
206 quote! { #[::tokio::main] }
207 } else {
208 quote! { #[::tokio::main(#args)] }
209 };
210
211 let body = input.block.clone();
213 input.block = syn::parse2(quote! {{
214 {
215 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
216 let _ = tracing_subscriber::fmt()
219 .with_env_filter(
220 EnvFilter::try_from_default_env()
221 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
222 )
223 .try_init();
224 }
225 #body
226 }})?;
227
228 Ok(quote! {
229 #tokio_attr
230 #input
231 })
232}
233
234fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
235 let path_str = if input.is_empty() {
237 "./migrations".to_string()
238 } else {
239 let lit: LitStr = syn::parse2(input)?;
240 lit.value()
241 };
242
243 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
244 syn::Error::new(
245 proc_macro2::Span::call_site(),
246 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
247 )
248 })?;
249 let abs = std::path::Path::new(&manifest).join(&path_str);
250
251 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
252 if abs.is_dir() {
253 let read = std::fs::read_dir(&abs).map_err(|e| {
254 syn::Error::new(
255 proc_macro2::Span::call_site(),
256 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
257 )
258 })?;
259 for entry in read.flatten() {
260 let path = entry.path();
261 if !path.is_file() {
262 continue;
263 }
264 if path.extension().and_then(|s| s.to_str()) != Some("json") {
265 continue;
266 }
267 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
268 continue;
269 };
270 entries.push((stem.to_owned(), path));
271 }
272 }
273 entries.sort_by(|a, b| a.0.cmp(&b.0));
274
275 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
288 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
289 for (stem, path) in &entries {
290 let raw = std::fs::read_to_string(path).map_err(|e| {
291 syn::Error::new(
292 proc_macro2::Span::call_site(),
293 format!(
294 "embed_migrations!: cannot read {} for chain validation: {e}",
295 path.display()
296 ),
297 )
298 })?;
299 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
300 syn::Error::new(
301 proc_macro2::Span::call_site(),
302 format!(
303 "embed_migrations!: {} is not valid JSON: {e}",
304 path.display()
305 ),
306 )
307 })?;
308 let name = json
309 .get("name")
310 .and_then(|v| v.as_str())
311 .ok_or_else(|| {
312 syn::Error::new(
313 proc_macro2::Span::call_site(),
314 format!(
315 "embed_migrations!: {} is missing the `name` field",
316 path.display()
317 ),
318 )
319 })?
320 .to_owned();
321 if name != *stem {
322 return Err(syn::Error::new(
323 proc_macro2::Span::call_site(),
324 format!(
325 "embed_migrations!: file stem `{stem}` does not match the migration's \
326 `name` field `{name}` — rename the file or fix the JSON",
327 ),
328 ));
329 }
330 let prev = json
331 .get("prev")
332 .and_then(|v| v.as_str())
333 .map(str::to_owned);
334 chain_names.push(name.clone());
335 prev_refs.push((name, prev));
336 }
337
338 let name_set: std::collections::HashSet<&str> =
339 chain_names.iter().map(String::as_str).collect();
340 for (name, prev) in &prev_refs {
341 if let Some(p) = prev {
342 if !name_set.contains(p.as_str()) {
343 return Err(syn::Error::new(
344 proc_macro2::Span::call_site(),
345 format!(
346 "embed_migrations!: broken migration chain — `{name}` declares \
347 prev=`{p}` but no migration with that name exists in {}",
348 abs.display()
349 ),
350 ));
351 }
352 }
353 }
354
355 let pairs: Vec<TokenStream2> = entries
356 .iter()
357 .map(|(name, path)| {
358 let path_lit = path.display().to_string();
359 quote! { (#name, ::core::include_str!(#path_lit)) }
360 })
361 .collect();
362
363 Ok(quote! {
364 {
365 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
366 __RUSTANGO_EMBEDDED
367 }
368 })
369}
370
371fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
372 let struct_name = &input.ident;
373
374 let Data::Struct(data) = &input.data else {
375 return Err(syn::Error::new_spanned(
376 struct_name,
377 "Model can only be derived on structs",
378 ));
379 };
380 let Fields::Named(named) = &data.fields else {
381 return Err(syn::Error::new_spanned(
382 struct_name,
383 "Model requires a struct with named fields",
384 ));
385 };
386
387 let container = parse_container_attrs(input)?;
388 let table = container
389 .table
390 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
391 let model_name = struct_name.to_string();
392
393 let collected = collect_fields(named)?;
394
395 if let Some((ref display, span)) = container.display {
397 if !collected.field_names.iter().any(|n| n == display) {
398 return Err(syn::Error::new(
399 span,
400 format!("`display = \"{display}\"` does not match any field on this struct"),
401 ));
402 }
403 }
404 let display = container.display.map(|(name, _)| name);
405 let app_label = container.app.clone();
406
407 if let Some(admin) = &container.admin {
409 for (label, list) in [
410 ("list_display", &admin.list_display),
411 ("search_fields", &admin.search_fields),
412 ("readonly_fields", &admin.readonly_fields),
413 ("list_filter", &admin.list_filter),
414 ] {
415 if let Some((names, span)) = list {
416 for name in names {
417 if !collected.field_names.iter().any(|n| n == name) {
418 return Err(syn::Error::new(
419 *span,
420 format!(
421 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
422 ),
423 ));
424 }
425 }
426 }
427 }
428 if let Some((pairs, span)) = &admin.ordering {
429 for (name, _) in pairs {
430 if !collected.field_names.iter().any(|n| n == name) {
431 return Err(syn::Error::new(
432 *span,
433 format!(
434 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
435 ),
436 ));
437 }
438 }
439 }
440 if let Some((groups, span)) = &admin.fieldsets {
441 for (_, fields) in groups {
442 for name in fields {
443 if !collected.field_names.iter().any(|n| n == name) {
444 return Err(syn::Error::new(
445 *span,
446 format!(
447 "`fieldsets`: \"{name}\" is not a declared field on this struct"
448 ),
449 ));
450 }
451 }
452 }
453 }
454 }
455 if let Some(audit) = &container.audit {
456 if let Some((names, span)) = &audit.track {
457 for name in names {
458 if !collected.field_names.iter().any(|n| n == name) {
459 return Err(syn::Error::new(
460 *span,
461 format!(
462 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
463 ),
464 ));
465 }
466 }
467 }
468 }
469
470 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
473 audit
474 .track
475 .as_ref()
476 .map(|(names, _)| names.clone())
477 .unwrap_or_default()
478 });
479
480 let mut all_indexes: Vec<IndexAttr> = container.indexes;
482 for field in &named.named {
483 let ident = field.ident.as_ref().expect("named");
484 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
487 if fa.index {
488 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
489 let auto_name = if fa.index_unique {
490 format!("{table}_{col_name}_uq_idx")
491 } else {
492 format!("{table}_{col_name}_idx")
493 };
494 all_indexes.push(IndexAttr {
495 name: fa.index_name.or(Some(auto_name)),
496 columns: vec![col_name],
497 unique: fa.index_unique,
498 });
499 }
500 }
501 }
502
503 let model_impl = model_impl_tokens(
504 struct_name,
505 &model_name,
506 &table,
507 display.as_deref(),
508 app_label.as_deref(),
509 container.admin.as_ref(),
510 &collected.field_schemas,
511 collected.soft_delete_column.as_deref(),
512 container.permissions,
513 audit_track_names.as_deref(),
514 &container.m2m,
515 &all_indexes,
516 &container.checks,
517 );
518 let module_ident = column_module_ident(struct_name);
519 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
520 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
521 let track_set: Option<std::collections::HashSet<&str>> = audit
522 .track
523 .as_ref()
524 .map(|(names, _)| names.iter().map(String::as_str).collect());
525 collected
526 .column_entries
527 .iter()
528 .filter(|c| {
529 track_set
530 .as_ref()
531 .map_or(true, |s| s.contains(c.name.as_str()))
532 })
533 .collect()
534 });
535 let inherent_impl = inherent_impl_tokens(
536 struct_name,
537 &collected,
538 collected.primary_key.as_ref(),
539 &column_consts,
540 audited_fields.as_deref(),
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(
579 struct_name: &syn::Ident,
580 fk_relations: &[FkRelation],
581) -> TokenStream2 {
582 let arms = fk_relations.iter().map(|rel| {
583 let parent_ty = &rel.parent_type;
584 let fk_col = rel.fk_column.as_str();
585 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
588 quote! {
589 #fk_col => {
590 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
591 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
592 ::rustango::core::SqlValue::I64(v) => v,
593 _ => 0i64,
594 };
595 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
596 ::core::result::Result::Ok(true)
597 }
598 }
599 });
600 quote! {
601 impl ::rustango::sql::LoadRelated for #struct_name {
602 #[allow(unused_variables)]
603 fn __rustango_load_related(
604 &mut self,
605 row: &::rustango::sql::sqlx::postgres::PgRow,
606 field_name: &str,
607 alias: &str,
608 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
609 match field_name {
610 #( #arms )*
611 _ => ::core::result::Result::Ok(false),
612 }
613 }
614 }
615 }
616}
617
618fn fk_pk_access_impl_tokens(
626 struct_name: &syn::Ident,
627 fk_relations: &[FkRelation],
628) -> TokenStream2 {
629 let arms = fk_relations.iter().map(|rel| {
630 let fk_col = rel.fk_column.as_str();
631 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
632 quote! {
633 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
634 }
635 });
636 quote! {
637 impl ::rustango::sql::FkPkAccess for #struct_name {
638 #[allow(unused_variables)]
639 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
640 match field_name {
641 #( #arms )*
642 _ => ::core::option::Option::None,
643 }
644 }
645 }
646 }
647}
648
649fn reverse_helper_tokens(
655 child_ident: &syn::Ident,
656 fk_relations: &[FkRelation],
657) -> TokenStream2 {
658 if fk_relations.is_empty() {
659 return TokenStream2::new();
660 }
661 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
665 let method_ident = syn::Ident::new(&suffix, child_ident.span());
666 let impls = fk_relations.iter().map(|rel| {
667 let parent_ty = &rel.parent_type;
668 let fk_col = rel.fk_column.as_str();
669 let doc = format!(
670 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
671 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
672 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
673 further `{child_ident}::objects()` filters via direct queryset use."
674 );
675 quote! {
676 impl #parent_ty {
677 #[doc = #doc]
678 pub async fn #method_ident<'_c, _E>(
683 &self,
684 _executor: _E,
685 ) -> ::core::result::Result<
686 ::std::vec::Vec<#child_ident>,
687 ::rustango::sql::ExecError,
688 >
689 where
690 _E: ::rustango::sql::sqlx::Executor<
691 '_c,
692 Database = ::rustango::sql::sqlx::Postgres,
693 >,
694 {
695 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
696 ::rustango::query::QuerySet::<#child_ident>::new()
697 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
698 .fetch_on(_executor)
699 .await
700 }
701 }
702 }
703 });
704 quote! { #( #impls )* }
705}
706
707fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
710 if m2m_relations.is_empty() {
711 return TokenStream2::new();
712 }
713 let methods = m2m_relations.iter().map(|rel| {
714 let method_name = format!("{}_m2m", rel.name);
715 let method_ident = syn::Ident::new(&method_name, struct_name.span());
716 let through = rel.through.as_str();
717 let src_col = rel.src.as_str();
718 let dst_col = rel.dst.as_str();
719 quote! {
720 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
721 ::rustango::sql::M2MManager {
722 src_pk: self.__rustango_pk_value(),
723 through: #through,
724 src_col: #src_col,
725 dst_col: #dst_col,
726 }
727 }
728 }
729 });
730 quote! {
731 impl #struct_name {
732 #( #methods )*
733 }
734 }
735}
736
737struct ColumnEntry {
738 ident: syn::Ident,
741 value_ty: Type,
743 name: String,
745 column: String,
747 field_type_tokens: TokenStream2,
749}
750
751struct CollectedFields {
752 field_schemas: Vec<TokenStream2>,
753 from_row_inits: Vec<TokenStream2>,
754 from_aliased_row_inits: Vec<TokenStream2>,
758 insert_columns: Vec<TokenStream2>,
761 insert_values: Vec<TokenStream2>,
764 insert_pushes: Vec<TokenStream2>,
769 returning_cols: Vec<TokenStream2>,
772 auto_assigns: Vec<TokenStream2>,
775 auto_field_idents: Vec<(syn::Ident, String)>,
779 bulk_pushes_no_auto: Vec<TokenStream2>,
783 bulk_pushes_all: Vec<TokenStream2>,
787 bulk_columns_no_auto: Vec<TokenStream2>,
790 bulk_columns_all: Vec<TokenStream2>,
793 bulk_auto_uniformity: Vec<TokenStream2>,
797 first_auto_ident: Option<syn::Ident>,
800 has_auto: bool,
802 pk_is_auto: bool,
806 update_assignments: Vec<TokenStream2>,
809 upsert_update_columns: Vec<TokenStream2>,
812 primary_key: Option<(syn::Ident, String)>,
813 column_entries: Vec<ColumnEntry>,
814 field_names: Vec<String>,
817 fk_relations: Vec<FkRelation>,
822 soft_delete_column: Option<String>,
827}
828
829#[derive(Clone)]
830struct FkRelation {
831 parent_type: Type,
834 fk_column: String,
837}
838
839fn collect_fields(named: &syn::FieldsNamed) -> syn::Result<CollectedFields> {
840 let cap = named.named.len();
841 let mut out = CollectedFields {
842 field_schemas: Vec::with_capacity(cap),
843 from_row_inits: Vec::with_capacity(cap),
844 from_aliased_row_inits: Vec::with_capacity(cap),
845 insert_columns: Vec::with_capacity(cap),
846 insert_values: Vec::with_capacity(cap),
847 insert_pushes: Vec::with_capacity(cap),
848 returning_cols: Vec::new(),
849 auto_assigns: Vec::new(),
850 auto_field_idents: Vec::new(),
851 bulk_pushes_no_auto: Vec::with_capacity(cap),
852 bulk_pushes_all: Vec::with_capacity(cap),
853 bulk_columns_no_auto: Vec::with_capacity(cap),
854 bulk_columns_all: Vec::with_capacity(cap),
855 bulk_auto_uniformity: Vec::new(),
856 first_auto_ident: None,
857 has_auto: false,
858 pk_is_auto: false,
859 update_assignments: Vec::with_capacity(cap),
860 upsert_update_columns: Vec::with_capacity(cap),
861 primary_key: None,
862 column_entries: Vec::with_capacity(cap),
863 field_names: Vec::with_capacity(cap),
864 fk_relations: Vec::new(),
865 soft_delete_column: None,
866 };
867
868 for field in &named.named {
869 let info = process_field(field)?;
870 out.field_names.push(info.ident.to_string());
871 out.field_schemas.push(info.schema);
872 out.from_row_inits.push(info.from_row_init);
873 out.from_aliased_row_inits.push(info.from_aliased_row_init);
874 if let Some(parent_ty) = info.fk_inner.clone() {
875 out.fk_relations.push(FkRelation {
876 parent_type: parent_ty,
877 fk_column: info.column.clone(),
878 });
879 }
880 if info.soft_delete {
881 if out.soft_delete_column.is_some() {
882 return Err(syn::Error::new_spanned(
883 field,
884 "only one field may be marked `#[rustango(soft_delete)]`",
885 ));
886 }
887 out.soft_delete_column = Some(info.column.clone());
888 }
889 let column = info.column.as_str();
890 let ident = info.ident;
891 out.insert_columns.push(quote!(#column));
892 out.insert_values.push(quote! {
893 ::core::convert::Into::<::rustango::core::SqlValue>::into(
894 ::core::clone::Clone::clone(&self.#ident)
895 )
896 });
897 if info.auto {
898 out.has_auto = true;
899 if out.first_auto_ident.is_none() {
900 out.first_auto_ident = Some(ident.clone());
901 }
902 out.returning_cols.push(quote!(#column));
903 out.auto_field_idents
904 .push((ident.clone(), info.column.clone()));
905 out.auto_assigns.push(quote! {
906 self.#ident = ::rustango::sql::sqlx::Row::try_get(&_returning_row, #column)?;
907 });
908 out.insert_pushes.push(quote! {
909 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
910 _columns.push(#column);
911 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
912 ::core::clone::Clone::clone(_v)
913 ));
914 }
915 });
916 out.bulk_columns_all.push(quote!(#column));
919 out.bulk_pushes_all.push(quote! {
920 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
921 ::core::clone::Clone::clone(&_row.#ident)
922 ));
923 });
924 let ident_clone = ident.clone();
928 out.bulk_auto_uniformity.push(quote! {
929 for _r in rows.iter().skip(1) {
930 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
931 return ::core::result::Result::Err(
932 ::rustango::sql::ExecError::Sql(
933 ::rustango::sql::SqlError::BulkAutoMixed
934 )
935 );
936 }
937 }
938 });
939 } else {
940 out.insert_pushes.push(quote! {
941 _columns.push(#column);
942 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
943 ::core::clone::Clone::clone(&self.#ident)
944 ));
945 });
946 out.bulk_columns_no_auto.push(quote!(#column));
948 out.bulk_columns_all.push(quote!(#column));
949 let push_expr = quote! {
950 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
951 ::core::clone::Clone::clone(&_row.#ident)
952 ));
953 };
954 out.bulk_pushes_no_auto.push(push_expr.clone());
955 out.bulk_pushes_all.push(push_expr);
956 }
957 if info.primary_key {
958 if out.primary_key.is_some() {
959 return Err(syn::Error::new_spanned(
960 field,
961 "only one field may be marked `#[rustango(primary_key)]`",
962 ));
963 }
964 out.primary_key = Some((ident.clone(), info.column.clone()));
965 if info.auto {
966 out.pk_is_auto = true;
967 }
968 } else if info.auto_now_add {
969 } else if info.auto_now {
971 out.update_assignments.push(quote! {
976 ::rustango::core::Assignment {
977 column: #column,
978 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
979 ::chrono::Utc::now()
980 ),
981 }
982 });
983 out.upsert_update_columns.push(quote!(#column));
984 } else {
985 out.update_assignments.push(quote! {
986 ::rustango::core::Assignment {
987 column: #column,
988 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
989 ::core::clone::Clone::clone(&self.#ident)
990 ),
991 }
992 });
993 out.upsert_update_columns.push(quote!(#column));
994 }
995 out.column_entries.push(ColumnEntry {
996 ident: ident.clone(),
997 value_ty: info.value_ty.clone(),
998 name: ident.to_string(),
999 column: info.column.clone(),
1000 field_type_tokens: info.field_type_tokens,
1001 });
1002 }
1003 Ok(out)
1004}
1005
1006fn model_impl_tokens(
1007 struct_name: &syn::Ident,
1008 model_name: &str,
1009 table: &str,
1010 display: Option<&str>,
1011 app_label: Option<&str>,
1012 admin: Option<&AdminAttrs>,
1013 field_schemas: &[TokenStream2],
1014 soft_delete_column: Option<&str>,
1015 permissions: bool,
1016 audit_track: Option<&[String]>,
1017 m2m_relations: &[M2MAttr],
1018 indexes: &[IndexAttr],
1019 checks: &[CheckAttr],
1020) -> TokenStream2 {
1021 let display_tokens = if let Some(name) = display {
1022 quote!(::core::option::Option::Some(#name))
1023 } else {
1024 quote!(::core::option::Option::None)
1025 };
1026 let app_label_tokens = if let Some(name) = app_label {
1027 quote!(::core::option::Option::Some(#name))
1028 } else {
1029 quote!(::core::option::Option::None)
1030 };
1031 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1032 quote!(::core::option::Option::Some(#col))
1033 } else {
1034 quote!(::core::option::Option::None)
1035 };
1036 let audit_track_tokens = match audit_track {
1037 None => quote!(::core::option::Option::None),
1038 Some(names) => {
1039 let lits = names.iter().map(|n| n.as_str());
1040 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1041 }
1042 };
1043 let admin_tokens = admin_config_tokens(admin);
1044 let indexes_tokens = indexes.iter().map(|idx| {
1045 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1046 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1047 let unique = idx.unique;
1048 quote! {
1049 ::rustango::core::IndexSchema {
1050 name: #name,
1051 columns: &[ #(#cols),* ],
1052 unique: #unique,
1053 }
1054 }
1055 });
1056 let checks_tokens = checks.iter().map(|c| {
1057 let name = c.name.as_str();
1058 let expr = c.expr.as_str();
1059 quote! {
1060 ::rustango::core::CheckConstraint {
1061 name: #name,
1062 expr: #expr,
1063 }
1064 }
1065 });
1066 let m2m_tokens = m2m_relations.iter().map(|rel| {
1067 let name = rel.name.as_str();
1068 let to = rel.to.as_str();
1069 let through = rel.through.as_str();
1070 let src = rel.src.as_str();
1071 let dst = rel.dst.as_str();
1072 quote! {
1073 ::rustango::core::M2MRelation {
1074 name: #name,
1075 to: #to,
1076 through: #through,
1077 src_col: #src,
1078 dst_col: #dst,
1079 }
1080 }
1081 });
1082 quote! {
1083 impl ::rustango::core::Model for #struct_name {
1084 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1085 name: #model_name,
1086 table: #table,
1087 fields: &[ #(#field_schemas),* ],
1088 display: #display_tokens,
1089 app_label: #app_label_tokens,
1090 admin: #admin_tokens,
1091 soft_delete_column: #soft_delete_tokens,
1092 permissions: #permissions,
1093 audit_track: #audit_track_tokens,
1094 m2m: &[ #(#m2m_tokens),* ],
1095 indexes: &[ #(#indexes_tokens),* ],
1096 check_constraints: &[ #(#checks_tokens),* ],
1097 };
1098 }
1099 }
1100}
1101
1102fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1106 let Some(admin) = admin else {
1107 return quote!(::core::option::Option::None);
1108 };
1109
1110 let list_display = admin
1111 .list_display
1112 .as_ref()
1113 .map(|(v, _)| v.as_slice())
1114 .unwrap_or(&[]);
1115 let list_display_lits = list_display.iter().map(|s| s.as_str());
1116
1117 let search_fields = admin
1118 .search_fields
1119 .as_ref()
1120 .map(|(v, _)| v.as_slice())
1121 .unwrap_or(&[]);
1122 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1123
1124 let readonly_fields = admin
1125 .readonly_fields
1126 .as_ref()
1127 .map(|(v, _)| v.as_slice())
1128 .unwrap_or(&[]);
1129 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1130
1131 let list_filter = admin
1132 .list_filter
1133 .as_ref()
1134 .map(|(v, _)| v.as_slice())
1135 .unwrap_or(&[]);
1136 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1137
1138 let actions = admin
1139 .actions
1140 .as_ref()
1141 .map(|(v, _)| v.as_slice())
1142 .unwrap_or(&[]);
1143 let actions_lits = actions.iter().map(|s| s.as_str());
1144
1145 let fieldsets = admin
1146 .fieldsets
1147 .as_ref()
1148 .map(|(v, _)| v.as_slice())
1149 .unwrap_or(&[]);
1150 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1151 let title = title.as_str();
1152 let field_lits = fields.iter().map(|s| s.as_str());
1153 quote!(::rustango::core::Fieldset {
1154 title: #title,
1155 fields: &[ #( #field_lits ),* ],
1156 })
1157 });
1158
1159 let list_per_page = admin.list_per_page.unwrap_or(0);
1160
1161 let ordering_pairs = admin
1162 .ordering
1163 .as_ref()
1164 .map(|(v, _)| v.as_slice())
1165 .unwrap_or(&[]);
1166 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1167 let name = name.as_str();
1168 let desc = *desc;
1169 quote!((#name, #desc))
1170 });
1171
1172 quote! {
1173 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1174 list_display: &[ #( #list_display_lits ),* ],
1175 search_fields: &[ #( #search_fields_lits ),* ],
1176 list_per_page: #list_per_page,
1177 ordering: &[ #( #ordering_tokens ),* ],
1178 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1179 list_filter: &[ #( #list_filter_lits ),* ],
1180 actions: &[ #( #actions_lits ),* ],
1181 fieldsets: &[ #( #fieldset_tokens ),* ],
1182 })
1183 }
1184}
1185
1186fn inherent_impl_tokens(
1187 struct_name: &syn::Ident,
1188 fields: &CollectedFields,
1189 primary_key: Option<&(syn::Ident, String)>,
1190 column_consts: &TokenStream2,
1191 audited_fields: Option<&[&ColumnEntry]>,
1192) -> TokenStream2 {
1193 let executor_passes_to_data_write = if audited_fields.is_some() {
1199 quote!(&mut *_executor)
1200 } else {
1201 quote!(_executor)
1202 };
1203 let executor_param = if audited_fields.is_some() {
1204 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1205 } else {
1206 quote!(_executor: _E)
1207 };
1208 let executor_generics = if audited_fields.is_some() {
1209 quote!()
1210 } else {
1211 quote!(<'_c, _E>)
1212 };
1213 let executor_where = if audited_fields.is_some() {
1214 quote!()
1215 } else {
1216 quote! {
1217 where
1218 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1219 }
1220 };
1221 let pool_to_save_on = if audited_fields.is_some() {
1226 quote! {
1227 let mut _conn = pool.acquire().await?;
1228 self.save_on(&mut *_conn).await
1229 }
1230 } else {
1231 quote!(self.save_on(pool).await)
1232 };
1233 let pool_to_insert_on = if audited_fields.is_some() {
1234 quote! {
1235 let mut _conn = pool.acquire().await?;
1236 self.insert_on(&mut *_conn).await
1237 }
1238 } else {
1239 quote!(self.insert_on(pool).await)
1240 };
1241 let pool_to_delete_on = if audited_fields.is_some() {
1242 quote! {
1243 let mut _conn = pool.acquire().await?;
1244 self.delete_on(&mut *_conn).await
1245 }
1246 } else {
1247 quote!(self.delete_on(pool).await)
1248 };
1249 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1250 quote! {
1251 let mut _conn = pool.acquire().await?;
1252 Self::bulk_insert_on(rows, &mut *_conn).await
1253 }
1254 } else {
1255 quote!(Self::bulk_insert_on(rows, pool).await)
1256 };
1257
1258 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1263 .map(|tracked| {
1264 tracked
1265 .iter()
1266 .map(|c| {
1267 let column_lit = c.column.as_str();
1268 let ident = &c.ident;
1269 quote! {
1270 (
1271 #column_lit,
1272 ::serde_json::to_value(&self.#ident)
1273 .unwrap_or(::serde_json::Value::Null),
1274 )
1275 }
1276 })
1277 .collect()
1278 })
1279 .unwrap_or_default();
1280 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1281 if fields.pk_is_auto {
1282 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1283 } else {
1284 quote!(::std::format!("{}", &self.#pk_ident))
1285 }
1286 } else {
1287 quote!(::std::string::String::new())
1288 };
1289 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1290 if audited_fields.is_some() {
1291 let pairs = audit_pair_tokens.iter();
1292 let pk_str = audit_pk_to_string.clone();
1293 quote! {
1294 let _audit_entry = ::rustango::audit::PendingEntry {
1295 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1296 entity_pk: #pk_str,
1297 operation: #op_path,
1298 source: ::rustango::audit::current_source(),
1299 changes: ::rustango::audit::snapshot_changes(&[
1300 #( #pairs ),*
1301 ]),
1302 };
1303 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1304 }
1305 } else {
1306 quote!()
1307 }
1308 };
1309 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1310 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1311 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1312 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1313
1314 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) =
1324 if let Some(tracked) = audited_fields {
1325 if tracked.is_empty() {
1326 (quote!(), quote!())
1327 } else {
1328 let select_cols: String = tracked
1329 .iter()
1330 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1331 .collect::<Vec<_>>()
1332 .join(", ");
1333 let pk_column_for_select = primary_key
1334 .map(|(_, col)| col.clone())
1335 .unwrap_or_default();
1336 let select_cols_lit = select_cols;
1337 let pk_column_lit_for_select = pk_column_for_select;
1338 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
1339 if fields.pk_is_auto {
1340 quote!(self.#pk_ident.get().copied().unwrap_or_default())
1341 } else {
1342 quote!(::core::clone::Clone::clone(&self.#pk_ident))
1343 }
1344 } else {
1345 quote!(0_i64)
1346 };
1347 let before_pairs = tracked.iter().map(|c| {
1348 let column_lit = c.column.as_str();
1349 let value_ty = &c.value_ty;
1350 quote! {
1351 (
1352 #column_lit,
1353 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
1354 &_audit_before_row, #column_lit,
1355 ) {
1356 ::core::result::Result::Ok(v) => {
1357 ::serde_json::to_value(&v)
1358 .unwrap_or(::serde_json::Value::Null)
1359 }
1360 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1361 },
1362 )
1363 }
1364 });
1365 let after_pairs = tracked.iter().map(|c| {
1366 let column_lit = c.column.as_str();
1367 let ident = &c.ident;
1368 quote! {
1369 (
1370 #column_lit,
1371 ::serde_json::to_value(&self.#ident)
1372 .unwrap_or(::serde_json::Value::Null),
1373 )
1374 }
1375 });
1376 let pk_str = audit_pk_to_string.clone();
1377 let pre = quote! {
1378 let _audit_select_sql = ::std::format!(
1379 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
1380 #select_cols_lit,
1381 <Self as ::rustango::core::Model>::SCHEMA.table,
1382 #pk_column_lit_for_select,
1383 );
1384 let _audit_before_pairs:
1385 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
1386 match ::rustango::sql::sqlx::query(&_audit_select_sql)
1387 .bind(#pk_value_for_bind)
1388 .fetch_optional(&mut *_executor)
1389 .await
1390 {
1391 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
1392 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
1393 }
1394 _ => ::core::option::Option::None,
1395 };
1396 };
1397 let post = quote! {
1398 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
1399 let _audit_after:
1400 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
1401 ::std::vec![ #( #after_pairs ),* ];
1402 let _audit_entry = ::rustango::audit::PendingEntry {
1403 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1404 entity_pk: #pk_str,
1405 operation: ::rustango::audit::AuditOp::Update,
1406 source: ::rustango::audit::current_source(),
1407 changes: ::rustango::audit::diff_changes(
1408 &_audit_before,
1409 &_audit_after,
1410 ),
1411 };
1412 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1413 }
1414 };
1415 (pre, post)
1416 }
1417 } else {
1418 (quote!(), quote!())
1419 };
1420
1421 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
1425 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
1426 if fields.pk_is_auto {
1427 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1428 } else {
1429 quote!(::std::format!("{}", &_row.#pk_ident))
1430 }
1431 } else {
1432 quote!(::std::string::String::new())
1433 };
1434 let row_pairs = audited_fields
1435 .unwrap_or(&[])
1436 .iter()
1437 .map(|c| {
1438 let column_lit = c.column.as_str();
1439 let ident = &c.ident;
1440 quote! {
1441 (
1442 #column_lit,
1443 ::serde_json::to_value(&_row.#ident)
1444 .unwrap_or(::serde_json::Value::Null),
1445 )
1446 }
1447 });
1448 quote! {
1449 let _audit_source = ::rustango::audit::current_source();
1450 let mut _audit_entries:
1451 ::std::vec::Vec<::rustango::audit::PendingEntry> =
1452 ::std::vec::Vec::with_capacity(rows.len());
1453 for _row in rows.iter() {
1454 _audit_entries.push(::rustango::audit::PendingEntry {
1455 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1456 entity_pk: #row_pk_str,
1457 operation: ::rustango::audit::AuditOp::Create,
1458 source: _audit_source.clone(),
1459 changes: ::rustango::audit::snapshot_changes(&[
1460 #( #row_pairs ),*
1461 ]),
1462 });
1463 }
1464 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
1465 }
1466 } else {
1467 quote!()
1468 };
1469
1470 let save_method = if fields.pk_is_auto {
1471 let (pk_ident, pk_column) = primary_key
1472 .expect("pk_is_auto implies primary_key is Some");
1473 let pk_column_lit = pk_column.as_str();
1474 let assignments = &fields.update_assignments;
1475 let upsert_cols = &fields.upsert_update_columns;
1476 let upsert_pushes = &fields.insert_pushes;
1477 let upsert_returning = &fields.returning_cols;
1478 let upsert_auto_assigns = &fields.auto_assigns;
1479 let conflict_clause = if fields.upsert_update_columns.is_empty() {
1480 quote!(::rustango::core::ConflictClause::DoNothing)
1481 } else {
1482 quote!(::rustango::core::ConflictClause::DoUpdate {
1483 target: ::std::vec![#pk_column_lit],
1484 update_columns: ::std::vec![ #( #upsert_cols ),* ],
1485 })
1486 };
1487 Some(quote! {
1488 pub async fn save(
1506 &mut self,
1507 pool: &::rustango::sql::sqlx::PgPool,
1508 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1509 #pool_to_save_on
1510 }
1511
1512 pub async fn save_on #executor_generics (
1523 &mut self,
1524 #executor_param,
1525 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1526 #executor_where
1527 {
1528 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1529 return self.insert_on(#executor_passes_to_data_write).await;
1530 }
1531 #audit_update_pre
1532 let _query = ::rustango::core::UpdateQuery {
1533 model: <Self as ::rustango::core::Model>::SCHEMA,
1534 set: ::std::vec![ #( #assignments ),* ],
1535 where_clause: ::rustango::core::WhereExpr::Predicate(
1536 ::rustango::core::Filter {
1537 column: #pk_column_lit,
1538 op: ::rustango::core::Op::Eq,
1539 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1540 ::core::clone::Clone::clone(&self.#pk_ident)
1541 ),
1542 }
1543 ),
1544 };
1545 let _ = ::rustango::sql::update_on(
1546 #executor_passes_to_data_write,
1547 &_query,
1548 ).await?;
1549 #audit_update_post
1550 ::core::result::Result::Ok(())
1551 }
1552
1553 pub async fn save_on_with #executor_generics (
1564 &mut self,
1565 #executor_param,
1566 source: ::rustango::audit::AuditSource,
1567 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1568 #executor_where
1569 {
1570 ::rustango::audit::with_source(source, self.save_on(_executor)).await
1571 }
1572
1573 pub async fn upsert(
1583 &mut self,
1584 pool: &::rustango::sql::sqlx::PgPool,
1585 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1586 self.upsert_on(pool).await
1587 }
1588
1589 pub async fn upsert_on #executor_generics (
1595 &mut self,
1596 #executor_param,
1597 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1598 #executor_where
1599 {
1600 let mut _columns: ::std::vec::Vec<&'static str> =
1601 ::std::vec::Vec::new();
1602 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1603 ::std::vec::Vec::new();
1604 #( #upsert_pushes )*
1605 let query = ::rustango::core::InsertQuery {
1606 model: <Self as ::rustango::core::Model>::SCHEMA,
1607 columns: _columns,
1608 values: _values,
1609 returning: ::std::vec![ #( #upsert_returning ),* ],
1610 on_conflict: ::core::option::Option::Some(#conflict_clause),
1611 };
1612 let _returning_row = ::rustango::sql::insert_returning_on(
1613 #executor_passes_to_data_write,
1614 &query,
1615 ).await?;
1616 #( #upsert_auto_assigns )*
1617 ::core::result::Result::Ok(())
1618 }
1619 })
1620 } else {
1621 None
1622 };
1623
1624 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
1625 let pk_column_lit = pk_column.as_str();
1626 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
1633 let col_lit = col;
1634 quote! {
1635 pub async fn soft_delete_on #executor_generics (
1645 &self,
1646 #executor_param,
1647 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1648 #executor_where
1649 {
1650 let _query = ::rustango::core::UpdateQuery {
1651 model: <Self as ::rustango::core::Model>::SCHEMA,
1652 set: ::std::vec![
1653 ::rustango::core::Assignment {
1654 column: #col_lit,
1655 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1656 ::chrono::Utc::now()
1657 ),
1658 },
1659 ],
1660 where_clause: ::rustango::core::WhereExpr::Predicate(
1661 ::rustango::core::Filter {
1662 column: #pk_column_lit,
1663 op: ::rustango::core::Op::Eq,
1664 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1665 ::core::clone::Clone::clone(&self.#pk_ident)
1666 ),
1667 }
1668 ),
1669 };
1670 let _affected = ::rustango::sql::update_on(
1671 #executor_passes_to_data_write,
1672 &_query,
1673 ).await?;
1674 #audit_softdelete_emit
1675 ::core::result::Result::Ok(_affected)
1676 }
1677
1678 pub async fn restore_on #executor_generics (
1685 &self,
1686 #executor_param,
1687 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1688 #executor_where
1689 {
1690 let _query = ::rustango::core::UpdateQuery {
1691 model: <Self as ::rustango::core::Model>::SCHEMA,
1692 set: ::std::vec![
1693 ::rustango::core::Assignment {
1694 column: #col_lit,
1695 value: ::rustango::core::SqlValue::Null,
1696 },
1697 ],
1698 where_clause: ::rustango::core::WhereExpr::Predicate(
1699 ::rustango::core::Filter {
1700 column: #pk_column_lit,
1701 op: ::rustango::core::Op::Eq,
1702 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1703 ::core::clone::Clone::clone(&self.#pk_ident)
1704 ),
1705 }
1706 ),
1707 };
1708 let _affected = ::rustango::sql::update_on(
1709 #executor_passes_to_data_write,
1710 &_query,
1711 ).await?;
1712 #audit_restore_emit
1713 ::core::result::Result::Ok(_affected)
1714 }
1715 }
1716 } else {
1717 quote!()
1718 };
1719 quote! {
1720 pub async fn delete(
1728 &self,
1729 pool: &::rustango::sql::sqlx::PgPool,
1730 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1731 #pool_to_delete_on
1732 }
1733
1734 pub async fn delete_on #executor_generics (
1741 &self,
1742 #executor_param,
1743 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1744 #executor_where
1745 {
1746 let query = ::rustango::core::DeleteQuery {
1747 model: <Self as ::rustango::core::Model>::SCHEMA,
1748 where_clause: ::rustango::core::WhereExpr::Predicate(
1749 ::rustango::core::Filter {
1750 column: #pk_column_lit,
1751 op: ::rustango::core::Op::Eq,
1752 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1753 ::core::clone::Clone::clone(&self.#pk_ident)
1754 ),
1755 }
1756 ),
1757 };
1758 let _affected = ::rustango::sql::delete_on(
1759 #executor_passes_to_data_write,
1760 &query,
1761 ).await?;
1762 #audit_delete_emit
1763 ::core::result::Result::Ok(_affected)
1764 }
1765
1766 pub async fn delete_on_with #executor_generics (
1772 &self,
1773 #executor_param,
1774 source: ::rustango::audit::AuditSource,
1775 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1776 #executor_where
1777 {
1778 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
1779 }
1780 #soft_delete_methods
1781 }
1782 });
1783
1784 let insert_method = if fields.has_auto {
1785 let pushes = &fields.insert_pushes;
1786 let returning_cols = &fields.returning_cols;
1787 let auto_assigns = &fields.auto_assigns;
1788 quote! {
1789 pub async fn insert(
1798 &mut self,
1799 pool: &::rustango::sql::sqlx::PgPool,
1800 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1801 #pool_to_insert_on
1802 }
1803
1804 pub async fn insert_on #executor_generics (
1810 &mut self,
1811 #executor_param,
1812 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1813 #executor_where
1814 {
1815 let mut _columns: ::std::vec::Vec<&'static str> =
1816 ::std::vec::Vec::new();
1817 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1818 ::std::vec::Vec::new();
1819 #( #pushes )*
1820 let query = ::rustango::core::InsertQuery {
1821 model: <Self as ::rustango::core::Model>::SCHEMA,
1822 columns: _columns,
1823 values: _values,
1824 returning: ::std::vec![ #( #returning_cols ),* ],
1825 on_conflict: ::core::option::Option::None,
1826 };
1827 let _returning_row = ::rustango::sql::insert_returning_on(
1828 #executor_passes_to_data_write,
1829 &query,
1830 ).await?;
1831 #( #auto_assigns )*
1832 #audit_insert_emit
1833 ::core::result::Result::Ok(())
1834 }
1835
1836 pub async fn insert_on_with #executor_generics (
1842 &mut self,
1843 #executor_param,
1844 source: ::rustango::audit::AuditSource,
1845 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1846 #executor_where
1847 {
1848 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
1849 }
1850 }
1851 } else {
1852 let insert_columns = &fields.insert_columns;
1853 let insert_values = &fields.insert_values;
1854 quote! {
1855 pub async fn insert(
1861 &self,
1862 pool: &::rustango::sql::sqlx::PgPool,
1863 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1864 self.insert_on(pool).await
1865 }
1866
1867 pub async fn insert_on<'_c, _E>(
1873 &self,
1874 _executor: _E,
1875 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1876 where
1877 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1878 {
1879 let query = ::rustango::core::InsertQuery {
1880 model: <Self as ::rustango::core::Model>::SCHEMA,
1881 columns: ::std::vec![ #( #insert_columns ),* ],
1882 values: ::std::vec![ #( #insert_values ),* ],
1883 returning: ::std::vec::Vec::new(),
1884 on_conflict: ::core::option::Option::None,
1885 };
1886 ::rustango::sql::insert_on(_executor, &query).await
1887 }
1888 }
1889 };
1890
1891 let bulk_insert_method = if fields.has_auto {
1892 let cols_no_auto = &fields.bulk_columns_no_auto;
1893 let cols_all = &fields.bulk_columns_all;
1894 let pushes_no_auto = &fields.bulk_pushes_no_auto;
1895 let pushes_all = &fields.bulk_pushes_all;
1896 let returning_cols = &fields.returning_cols;
1897 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
1898 let uniformity = &fields.bulk_auto_uniformity;
1899 let first_auto_ident = fields
1900 .first_auto_ident
1901 .as_ref()
1902 .expect("has_auto implies first_auto_ident is Some");
1903 quote! {
1904 pub async fn bulk_insert(
1918 rows: &mut [Self],
1919 pool: &::rustango::sql::sqlx::PgPool,
1920 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1921 #pool_to_bulk_insert_on
1922 }
1923
1924 pub async fn bulk_insert_on #executor_generics (
1930 rows: &mut [Self],
1931 #executor_param,
1932 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1933 #executor_where
1934 {
1935 if rows.is_empty() {
1936 return ::core::result::Result::Ok(());
1937 }
1938 let _first_unset = matches!(
1939 rows[0].#first_auto_ident,
1940 ::rustango::sql::Auto::Unset
1941 );
1942 #( #uniformity )*
1943
1944 let mut _all_rows: ::std::vec::Vec<
1945 ::std::vec::Vec<::rustango::core::SqlValue>,
1946 > = ::std::vec::Vec::with_capacity(rows.len());
1947 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
1948 for _row in rows.iter() {
1949 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1950 ::std::vec::Vec::new();
1951 #( #pushes_no_auto )*
1952 _all_rows.push(_row_vals);
1953 }
1954 ::std::vec![ #( #cols_no_auto ),* ]
1955 } else {
1956 for _row in rows.iter() {
1957 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1958 ::std::vec::Vec::new();
1959 #( #pushes_all )*
1960 _all_rows.push(_row_vals);
1961 }
1962 ::std::vec![ #( #cols_all ),* ]
1963 };
1964
1965 let _query = ::rustango::core::BulkInsertQuery {
1966 model: <Self as ::rustango::core::Model>::SCHEMA,
1967 columns: _columns,
1968 rows: _all_rows,
1969 returning: ::std::vec![ #( #returning_cols ),* ],
1970 on_conflict: ::core::option::Option::None,
1971 };
1972 let _returned = ::rustango::sql::bulk_insert_on(
1973 #executor_passes_to_data_write,
1974 &_query,
1975 ).await?;
1976 if _returned.len() != rows.len() {
1977 return ::core::result::Result::Err(
1978 ::rustango::sql::ExecError::Sql(
1979 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
1980 expected: rows.len(),
1981 actual: _returned.len(),
1982 }
1983 )
1984 );
1985 }
1986 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
1987 #auto_assigns_for_row
1988 }
1989 #audit_bulk_insert_emit
1990 ::core::result::Result::Ok(())
1991 }
1992 }
1993 } else {
1994 let cols_all = &fields.bulk_columns_all;
1995 let pushes_all = &fields.bulk_pushes_all;
1996 quote! {
1997 pub async fn bulk_insert(
2007 rows: &[Self],
2008 pool: &::rustango::sql::sqlx::PgPool,
2009 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2010 Self::bulk_insert_on(rows, pool).await
2011 }
2012
2013 pub async fn bulk_insert_on<'_c, _E>(
2019 rows: &[Self],
2020 _executor: _E,
2021 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2022 where
2023 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2024 {
2025 if rows.is_empty() {
2026 return ::core::result::Result::Ok(());
2027 }
2028 let mut _all_rows: ::std::vec::Vec<
2029 ::std::vec::Vec<::rustango::core::SqlValue>,
2030 > = ::std::vec::Vec::with_capacity(rows.len());
2031 for _row in rows.iter() {
2032 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2033 ::std::vec::Vec::new();
2034 #( #pushes_all )*
2035 _all_rows.push(_row_vals);
2036 }
2037 let _query = ::rustango::core::BulkInsertQuery {
2038 model: <Self as ::rustango::core::Model>::SCHEMA,
2039 columns: ::std::vec![ #( #cols_all ),* ],
2040 rows: _all_rows,
2041 returning: ::std::vec::Vec::new(),
2042 on_conflict: ::core::option::Option::None,
2043 };
2044 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2045 ::core::result::Result::Ok(())
2046 }
2047 }
2048 };
2049
2050 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2051 quote! {
2052 #[doc(hidden)]
2057 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2058 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2059 ::core::clone::Clone::clone(&self.#pk_ident)
2060 )
2061 }
2062 }
2063 });
2064
2065 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2066 quote! {
2067 impl ::rustango::sql::HasPkValue for #struct_name {
2068 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2069 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2070 ::core::clone::Clone::clone(&self.#pk_ident)
2071 )
2072 }
2073 }
2074 }
2075 });
2076
2077 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2078
2079 let from_aliased_row_inits = &fields.from_aliased_row_inits;
2080 let aliased_row_helper = quote! {
2081 #[doc(hidden)]
2087 pub fn __rustango_from_aliased_row(
2088 row: &::rustango::sql::sqlx::postgres::PgRow,
2089 prefix: &str,
2090 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2091 ::core::result::Result::Ok(Self {
2092 #( #from_aliased_row_inits ),*
2093 })
2094 }
2095 };
2096
2097 let load_related_impl =
2098 load_related_impl_tokens(struct_name, &fields.fk_relations);
2099
2100 quote! {
2101 impl #struct_name {
2102 #[must_use]
2104 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
2105 ::rustango::query::QuerySet::new()
2106 }
2107
2108 #insert_method
2109
2110 #bulk_insert_method
2111
2112 #save_method
2113
2114 #pk_methods
2115
2116 #pk_value_helper
2117
2118 #aliased_row_helper
2119
2120 #column_consts
2121 }
2122
2123 #load_related_impl
2124
2125 #has_pk_value_impl
2126
2127 #fk_pk_access_impl
2128 }
2129}
2130
2131fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
2135 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
2136 let col_lit = column.as_str();
2137 quote! {
2138 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
2139 _returning_row,
2140 #col_lit,
2141 )?;
2142 }
2143 });
2144 quote! { #( #lines )* }
2145}
2146
2147fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
2149 let lines = entries.iter().map(|e| {
2150 let ident = &e.ident;
2151 let col_ty = column_type_ident(ident);
2152 quote! {
2153 #[allow(non_upper_case_globals)]
2154 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
2155 }
2156 });
2157 quote! { #(#lines)* }
2158}
2159
2160fn column_module_tokens(
2163 module_ident: &syn::Ident,
2164 struct_name: &syn::Ident,
2165 entries: &[ColumnEntry],
2166) -> TokenStream2 {
2167 let items = entries.iter().map(|e| {
2168 let col_ty = column_type_ident(&e.ident);
2169 let value_ty = &e.value_ty;
2170 let name = &e.name;
2171 let column = &e.column;
2172 let field_type_tokens = &e.field_type_tokens;
2173 quote! {
2174 #[derive(::core::clone::Clone, ::core::marker::Copy)]
2175 pub struct #col_ty;
2176
2177 impl ::rustango::core::Column for #col_ty {
2178 type Model = super::#struct_name;
2179 type Value = #value_ty;
2180 const NAME: &'static str = #name;
2181 const COLUMN: &'static str = #column;
2182 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
2183 }
2184 }
2185 });
2186 quote! {
2187 #[doc(hidden)]
2188 #[allow(non_camel_case_types, non_snake_case)]
2189 pub mod #module_ident {
2190 #[allow(unused_imports)]
2195 use super::*;
2196 #(#items)*
2197 }
2198 }
2199}
2200
2201fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
2202 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
2203}
2204
2205fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
2206 syn::Ident::new(
2207 &format!("__rustango_cols_{struct_name}"),
2208 struct_name.span(),
2209 )
2210}
2211
2212fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
2213 quote! {
2214 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
2215 for #struct_name
2216 {
2217 fn from_row(
2218 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
2219 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2220 ::core::result::Result::Ok(Self {
2221 #( #from_row_inits ),*
2222 })
2223 }
2224 }
2225 }
2226}
2227
2228struct ContainerAttrs {
2229 table: Option<String>,
2230 display: Option<(String, proc_macro2::Span)>,
2231 app: Option<String>,
2238 admin: Option<AdminAttrs>,
2243 audit: Option<AuditAttrs>,
2249 permissions: bool,
2253 m2m: Vec<M2MAttr>,
2257 indexes: Vec<IndexAttr>,
2263 checks: Vec<CheckAttr>,
2266}
2267
2268struct IndexAttr {
2270 name: Option<String>,
2272 columns: Vec<String>,
2274 unique: bool,
2276}
2277
2278struct CheckAttr {
2280 name: String,
2281 expr: String,
2282}
2283
2284struct M2MAttr {
2286 name: String,
2288 to: String,
2290 through: String,
2292 src: String,
2294 dst: String,
2296}
2297
2298#[derive(Default)]
2304struct AuditAttrs {
2305 track: Option<(Vec<String>, proc_macro2::Span)>,
2309}
2310
2311#[derive(Default)]
2316struct AdminAttrs {
2317 list_display: Option<(Vec<String>, proc_macro2::Span)>,
2318 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
2319 list_per_page: Option<usize>,
2320 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
2321 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
2322 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
2323 actions: Option<(Vec<String>, proc_macro2::Span)>,
2326 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
2330}
2331
2332fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
2333 let mut out = ContainerAttrs {
2334 table: None,
2335 display: None,
2336 app: None,
2337 admin: None,
2338 audit: None,
2339 permissions: false,
2340 m2m: Vec::new(),
2341 indexes: Vec::new(),
2342 checks: Vec::new(),
2343 };
2344 for attr in &input.attrs {
2345 if !attr.path().is_ident("rustango") {
2346 continue;
2347 }
2348 attr.parse_nested_meta(|meta| {
2349 if meta.path.is_ident("table") {
2350 let s: LitStr = meta.value()?.parse()?;
2351 out.table = Some(s.value());
2352 return Ok(());
2353 }
2354 if meta.path.is_ident("display") {
2355 let s: LitStr = meta.value()?.parse()?;
2356 out.display = Some((s.value(), s.span()));
2357 return Ok(());
2358 }
2359 if meta.path.is_ident("app") {
2360 let s: LitStr = meta.value()?.parse()?;
2361 out.app = Some(s.value());
2362 return Ok(());
2363 }
2364 if meta.path.is_ident("admin") {
2365 let mut admin = AdminAttrs::default();
2366 meta.parse_nested_meta(|inner| {
2367 if inner.path.is_ident("list_display") {
2368 let s: LitStr = inner.value()?.parse()?;
2369 admin.list_display =
2370 Some((split_field_list(&s.value()), s.span()));
2371 return Ok(());
2372 }
2373 if inner.path.is_ident("search_fields") {
2374 let s: LitStr = inner.value()?.parse()?;
2375 admin.search_fields =
2376 Some((split_field_list(&s.value()), s.span()));
2377 return Ok(());
2378 }
2379 if inner.path.is_ident("readonly_fields") {
2380 let s: LitStr = inner.value()?.parse()?;
2381 admin.readonly_fields =
2382 Some((split_field_list(&s.value()), s.span()));
2383 return Ok(());
2384 }
2385 if inner.path.is_ident("list_per_page") {
2386 let lit: syn::LitInt = inner.value()?.parse()?;
2387 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
2388 return Ok(());
2389 }
2390 if inner.path.is_ident("ordering") {
2391 let s: LitStr = inner.value()?.parse()?;
2392 admin.ordering = Some((
2393 parse_ordering_list(&s.value()),
2394 s.span(),
2395 ));
2396 return Ok(());
2397 }
2398 if inner.path.is_ident("list_filter") {
2399 let s: LitStr = inner.value()?.parse()?;
2400 admin.list_filter =
2401 Some((split_field_list(&s.value()), s.span()));
2402 return Ok(());
2403 }
2404 if inner.path.is_ident("actions") {
2405 let s: LitStr = inner.value()?.parse()?;
2406 admin.actions =
2407 Some((split_field_list(&s.value()), s.span()));
2408 return Ok(());
2409 }
2410 if inner.path.is_ident("fieldsets") {
2411 let s: LitStr = inner.value()?.parse()?;
2412 admin.fieldsets =
2413 Some((parse_fieldset_list(&s.value()), s.span()));
2414 return Ok(());
2415 }
2416 Err(inner.error(
2417 "unknown admin attribute (supported: \
2418 `list_display`, `search_fields`, `readonly_fields`, \
2419 `list_filter`, `list_per_page`, `ordering`, `actions`, \
2420 `fieldsets`)",
2421 ))
2422 })?;
2423 out.admin = Some(admin);
2424 return Ok(());
2425 }
2426 if meta.path.is_ident("audit") {
2427 let mut audit = AuditAttrs::default();
2428 meta.parse_nested_meta(|inner| {
2429 if inner.path.is_ident("track") {
2430 let s: LitStr = inner.value()?.parse()?;
2431 audit.track =
2432 Some((split_field_list(&s.value()), s.span()));
2433 return Ok(());
2434 }
2435 Err(inner.error(
2436 "unknown audit attribute (supported: `track`)",
2437 ))
2438 })?;
2439 out.audit = Some(audit);
2440 return Ok(());
2441 }
2442 if meta.path.is_ident("permissions") {
2443 out.permissions = true;
2444 return Ok(());
2445 }
2446 if meta.path.is_ident("index") {
2447 let cols_lit: LitStr = meta.value()?.parse()?;
2451 let columns = split_field_list(&cols_lit.value());
2452 let mut unique = false;
2453 let mut name: Option<String> = None;
2454 if meta.input.peek(syn::Token![,]) {
2455 let _: syn::Token![,] = meta.input.parse()?;
2456 meta.parse_nested_meta(|inner| {
2458 if inner.path.is_ident("unique") {
2459 unique = true;
2460 return Ok(());
2461 }
2462 if inner.path.is_ident("name") {
2463 let s: LitStr = inner.value()?.parse()?;
2464 name = Some(s.value());
2465 return Ok(());
2466 }
2467 Err(inner.error("unknown index attribute (supported: `unique`, `name`)"))
2468 })?;
2469 }
2470 out.indexes.push(IndexAttr { name, columns, unique });
2471 return Ok(());
2472 }
2473 if meta.path.is_ident("check") {
2474 let mut name: Option<String> = None;
2476 let mut expr: Option<String> = None;
2477 meta.parse_nested_meta(|inner| {
2478 if inner.path.is_ident("name") {
2479 let s: LitStr = inner.value()?.parse()?;
2480 name = Some(s.value());
2481 return Ok(());
2482 }
2483 if inner.path.is_ident("expr") {
2484 let s: LitStr = inner.value()?.parse()?;
2485 expr = Some(s.value());
2486 return Ok(());
2487 }
2488 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
2489 })?;
2490 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
2491 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
2492 out.checks.push(CheckAttr { name, expr });
2493 return Ok(());
2494 }
2495 if meta.path.is_ident("m2m") {
2496 let mut m2m = M2MAttr {
2497 name: String::new(),
2498 to: String::new(),
2499 through: String::new(),
2500 src: String::new(),
2501 dst: String::new(),
2502 };
2503 meta.parse_nested_meta(|inner| {
2504 if inner.path.is_ident("name") {
2505 let s: LitStr = inner.value()?.parse()?;
2506 m2m.name = s.value();
2507 return Ok(());
2508 }
2509 if inner.path.is_ident("to") {
2510 let s: LitStr = inner.value()?.parse()?;
2511 m2m.to = s.value();
2512 return Ok(());
2513 }
2514 if inner.path.is_ident("through") {
2515 let s: LitStr = inner.value()?.parse()?;
2516 m2m.through = s.value();
2517 return Ok(());
2518 }
2519 if inner.path.is_ident("src") {
2520 let s: LitStr = inner.value()?.parse()?;
2521 m2m.src = s.value();
2522 return Ok(());
2523 }
2524 if inner.path.is_ident("dst") {
2525 let s: LitStr = inner.value()?.parse()?;
2526 m2m.dst = s.value();
2527 return Ok(());
2528 }
2529 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
2530 })?;
2531 if m2m.name.is_empty() {
2532 return Err(meta.error("m2m requires `name = \"...\"`"));
2533 }
2534 if m2m.to.is_empty() {
2535 return Err(meta.error("m2m requires `to = \"...\"`"));
2536 }
2537 if m2m.through.is_empty() {
2538 return Err(meta.error("m2m requires `through = \"...\"`"));
2539 }
2540 if m2m.src.is_empty() {
2541 return Err(meta.error("m2m requires `src = \"...\"`"));
2542 }
2543 if m2m.dst.is_empty() {
2544 return Err(meta.error("m2m requires `dst = \"...\"`"));
2545 }
2546 out.m2m.push(m2m);
2547 return Ok(());
2548 }
2549 Err(meta.error("unknown rustango container attribute"))
2550 })?;
2551 }
2552 Ok(out)
2553}
2554
2555fn split_field_list(raw: &str) -> Vec<String> {
2559 raw.split(',')
2560 .map(str::trim)
2561 .filter(|s| !s.is_empty())
2562 .map(str::to_owned)
2563 .collect()
2564}
2565
2566fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
2575 raw.split('|')
2576 .map(str::trim)
2577 .filter(|s| !s.is_empty())
2578 .map(|section| {
2579 let (title, rest) = match section.split_once(':') {
2581 Some((title, rest)) if !title.contains(',') => {
2582 (title.trim().to_owned(), rest)
2583 }
2584 _ => (String::new(), section),
2585 };
2586 let fields = split_field_list(rest);
2587 (title, fields)
2588 })
2589 .collect()
2590}
2591
2592fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
2595 raw.split(',')
2596 .map(str::trim)
2597 .filter(|s| !s.is_empty())
2598 .map(|spec| {
2599 spec.strip_prefix('-')
2600 .map_or((spec.to_owned(), false), |rest| (rest.trim().to_owned(), true))
2601 })
2602 .collect()
2603}
2604
2605struct FieldAttrs {
2606 column: Option<String>,
2607 primary_key: bool,
2608 fk: Option<String>,
2609 o2o: Option<String>,
2610 on: Option<String>,
2611 max_length: Option<u32>,
2612 min: Option<i64>,
2613 max: Option<i64>,
2614 default: Option<String>,
2615 auto_uuid: bool,
2621 auto_now_add: bool,
2626 auto_now: bool,
2632 soft_delete: bool,
2637 unique: bool,
2640 index: bool,
2644 index_unique: bool,
2645 index_name: Option<String>,
2646}
2647
2648fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
2649 let mut out = FieldAttrs {
2650 column: None,
2651 primary_key: false,
2652 fk: None,
2653 o2o: None,
2654 on: None,
2655 max_length: None,
2656 min: None,
2657 max: None,
2658 default: None,
2659 auto_uuid: false,
2660 auto_now_add: false,
2661 auto_now: false,
2662 soft_delete: false,
2663 unique: false,
2664 index: false,
2665 index_unique: false,
2666 index_name: None,
2667 };
2668 for attr in &field.attrs {
2669 if !attr.path().is_ident("rustango") {
2670 continue;
2671 }
2672 attr.parse_nested_meta(|meta| {
2673 if meta.path.is_ident("column") {
2674 let s: LitStr = meta.value()?.parse()?;
2675 out.column = Some(s.value());
2676 return Ok(());
2677 }
2678 if meta.path.is_ident("primary_key") {
2679 out.primary_key = true;
2680 return Ok(());
2681 }
2682 if meta.path.is_ident("fk") {
2683 let s: LitStr = meta.value()?.parse()?;
2684 out.fk = Some(s.value());
2685 return Ok(());
2686 }
2687 if meta.path.is_ident("o2o") {
2688 let s: LitStr = meta.value()?.parse()?;
2689 out.o2o = Some(s.value());
2690 return Ok(());
2691 }
2692 if meta.path.is_ident("on") {
2693 let s: LitStr = meta.value()?.parse()?;
2694 out.on = Some(s.value());
2695 return Ok(());
2696 }
2697 if meta.path.is_ident("max_length") {
2698 let lit: syn::LitInt = meta.value()?.parse()?;
2699 out.max_length = Some(lit.base10_parse::<u32>()?);
2700 return Ok(());
2701 }
2702 if meta.path.is_ident("min") {
2703 out.min = Some(parse_signed_i64(&meta)?);
2704 return Ok(());
2705 }
2706 if meta.path.is_ident("max") {
2707 out.max = Some(parse_signed_i64(&meta)?);
2708 return Ok(());
2709 }
2710 if meta.path.is_ident("default") {
2711 let s: LitStr = meta.value()?.parse()?;
2712 out.default = Some(s.value());
2713 return Ok(());
2714 }
2715 if meta.path.is_ident("auto_uuid") {
2716 out.auto_uuid = true;
2717 out.primary_key = true;
2721 if out.default.is_none() {
2722 out.default = Some("gen_random_uuid()".into());
2723 }
2724 return Ok(());
2725 }
2726 if meta.path.is_ident("auto_now_add") {
2727 out.auto_now_add = true;
2728 if out.default.is_none() {
2729 out.default = Some("now()".into());
2730 }
2731 return Ok(());
2732 }
2733 if meta.path.is_ident("auto_now") {
2734 out.auto_now = true;
2735 if out.default.is_none() {
2736 out.default = Some("now()".into());
2737 }
2738 return Ok(());
2739 }
2740 if meta.path.is_ident("soft_delete") {
2741 out.soft_delete = true;
2742 return Ok(());
2743 }
2744 if meta.path.is_ident("unique") {
2745 out.unique = true;
2746 return Ok(());
2747 }
2748 if meta.path.is_ident("index") {
2749 out.index = true;
2750 if meta.input.peek(syn::token::Paren) {
2752 meta.parse_nested_meta(|inner| {
2753 if inner.path.is_ident("unique") {
2754 out.index_unique = true;
2755 return Ok(());
2756 }
2757 if inner.path.is_ident("name") {
2758 let s: LitStr = inner.value()?.parse()?;
2759 out.index_name = Some(s.value());
2760 return Ok(());
2761 }
2762 Err(inner.error("unknown index sub-attribute (supported: `unique`, `name`)"))
2763 })?;
2764 }
2765 return Ok(());
2766 }
2767 Err(meta.error("unknown rustango field attribute"))
2768 })?;
2769 }
2770 Ok(out)
2771}
2772
2773fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
2775 let expr: syn::Expr = meta.value()?.parse()?;
2776 match expr {
2777 syn::Expr::Lit(syn::ExprLit {
2778 lit: syn::Lit::Int(lit),
2779 ..
2780 }) => lit.base10_parse::<i64>(),
2781 syn::Expr::Unary(syn::ExprUnary {
2782 op: syn::UnOp::Neg(_),
2783 expr,
2784 ..
2785 }) => {
2786 if let syn::Expr::Lit(syn::ExprLit {
2787 lit: syn::Lit::Int(lit),
2788 ..
2789 }) = *expr
2790 {
2791 let v: i64 = lit.base10_parse()?;
2792 Ok(-v)
2793 } else {
2794 Err(syn::Error::new_spanned(expr, "expected integer literal"))
2795 }
2796 }
2797 other => Err(syn::Error::new_spanned(
2798 other,
2799 "expected integer literal (signed)",
2800 )),
2801 }
2802}
2803
2804struct FieldInfo<'a> {
2805 ident: &'a syn::Ident,
2806 column: String,
2807 primary_key: bool,
2808 auto: bool,
2812 value_ty: &'a Type,
2815 field_type_tokens: TokenStream2,
2817 schema: TokenStream2,
2818 from_row_init: TokenStream2,
2819 from_aliased_row_init: TokenStream2,
2825 fk_inner: Option<Type>,
2829 auto_now: bool,
2835 auto_now_add: bool,
2841 soft_delete: bool,
2846}
2847
2848fn process_field(field: &syn::Field) -> syn::Result<FieldInfo<'_>> {
2849 let attrs = parse_field_attrs(field)?;
2850 let ident = field
2851 .ident
2852 .as_ref()
2853 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
2854 let name = ident.to_string();
2855 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
2856 let primary_key = attrs.primary_key;
2857 let DetectedType {
2858 kind,
2859 nullable,
2860 auto: detected_auto,
2861 fk_inner,
2862 } = detect_type(&field.ty)?;
2863 check_bound_compatibility(field, &attrs, kind)?;
2864 let auto = detected_auto;
2865 if attrs.auto_uuid {
2871 if kind != DetectedKind::Uuid {
2872 return Err(syn::Error::new_spanned(
2873 field,
2874 "`#[rustango(auto_uuid)]` requires the field type to be \
2875 `Auto<uuid::Uuid>`",
2876 ));
2877 }
2878 if !detected_auto {
2879 return Err(syn::Error::new_spanned(
2880 field,
2881 "`#[rustango(auto_uuid)]` requires the field type to be \
2882 wrapped in `Auto<...>` so the macro skips the column on \
2883 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
2884 ));
2885 }
2886 }
2887 if attrs.auto_now_add || attrs.auto_now {
2888 if kind != DetectedKind::DateTime {
2889 return Err(syn::Error::new_spanned(
2890 field,
2891 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2892 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
2893 ));
2894 }
2895 if !detected_auto {
2896 return Err(syn::Error::new_spanned(
2897 field,
2898 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2899 the field type to be wrapped in `Auto<...>` so the macro skips \
2900 the column on INSERT and the DB DEFAULT (`now()`) fires",
2901 ));
2902 }
2903 }
2904 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
2905 return Err(syn::Error::new_spanned(
2906 field,
2907 "`#[rustango(soft_delete)]` requires the field type to be \
2908 `Option<chrono::DateTime<chrono::Utc>>`",
2909 ));
2910 }
2911 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
2912 if detected_auto && !primary_key && !is_mixin_auto {
2913 return Err(syn::Error::new_spanned(
2914 field,
2915 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
2916 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
2917 `auto_now`",
2918 ));
2919 }
2920 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
2921 return Err(syn::Error::new_spanned(
2922 field,
2923 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
2924 SERIAL / BIGSERIAL already supplies a default sequence.",
2925 ));
2926 }
2927 if fk_inner.is_some() && primary_key {
2928 return Err(syn::Error::new_spanned(
2929 field,
2930 "`ForeignKey<T>` is not allowed on a primary-key field — \
2931 a row's PK is its own identity, not a reference to a parent.",
2932 ));
2933 }
2934 let relation = relation_tokens(field, &attrs, fk_inner)?;
2935 let column_lit = column.as_str();
2936 let field_type_tokens = kind.variant_tokens();
2937 let max_length = optional_u32(attrs.max_length);
2938 let min = optional_i64(attrs.min);
2939 let max = optional_i64(attrs.max);
2940 let default = optional_str(attrs.default.as_deref());
2941
2942 let unique = attrs.unique;
2943 let schema = quote! {
2944 ::rustango::core::FieldSchema {
2945 name: #name,
2946 column: #column_lit,
2947 ty: #field_type_tokens,
2948 nullable: #nullable,
2949 primary_key: #primary_key,
2950 relation: #relation,
2951 max_length: #max_length,
2952 min: #min,
2953 max: #max,
2954 default: #default,
2955 auto: #auto,
2956 unique: #unique,
2957 }
2958 };
2959
2960 let from_row_init = quote! {
2961 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
2962 };
2963 let from_aliased_row_init = quote! {
2964 #ident: ::rustango::sql::sqlx::Row::try_get(
2965 row,
2966 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
2967 )?
2968 };
2969
2970 Ok(FieldInfo {
2971 ident,
2972 column,
2973 primary_key,
2974 auto,
2975 value_ty: &field.ty,
2976 field_type_tokens,
2977 schema,
2978 from_row_init,
2979 from_aliased_row_init,
2980 fk_inner: fk_inner.cloned(),
2981 auto_now: attrs.auto_now,
2982 auto_now_add: attrs.auto_now_add,
2983 soft_delete: attrs.soft_delete,
2984 })
2985}
2986
2987fn check_bound_compatibility(
2988 field: &syn::Field,
2989 attrs: &FieldAttrs,
2990 kind: DetectedKind,
2991) -> syn::Result<()> {
2992 if attrs.max_length.is_some() && kind != DetectedKind::String {
2993 return Err(syn::Error::new_spanned(
2994 field,
2995 "`max_length` is only valid on `String` fields (or `Option<String>`)",
2996 ));
2997 }
2998 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
2999 return Err(syn::Error::new_spanned(
3000 field,
3001 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
3002 ));
3003 }
3004 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
3005 if min > max {
3006 return Err(syn::Error::new_spanned(
3007 field,
3008 format!("`min` ({min}) is greater than `max` ({max})"),
3009 ));
3010 }
3011 }
3012 Ok(())
3013}
3014
3015fn optional_u32(value: Option<u32>) -> TokenStream2 {
3016 if let Some(v) = value {
3017 quote!(::core::option::Option::Some(#v))
3018 } else {
3019 quote!(::core::option::Option::None)
3020 }
3021}
3022
3023fn optional_i64(value: Option<i64>) -> TokenStream2 {
3024 if let Some(v) = value {
3025 quote!(::core::option::Option::Some(#v))
3026 } else {
3027 quote!(::core::option::Option::None)
3028 }
3029}
3030
3031fn optional_str(value: Option<&str>) -> TokenStream2 {
3032 if let Some(v) = value {
3033 quote!(::core::option::Option::Some(#v))
3034 } else {
3035 quote!(::core::option::Option::None)
3036 }
3037}
3038
3039fn relation_tokens(
3040 field: &syn::Field,
3041 attrs: &FieldAttrs,
3042 fk_inner: Option<&syn::Type>,
3043) -> syn::Result<TokenStream2> {
3044 if let Some(inner) = fk_inner {
3045 if attrs.fk.is_some() || attrs.o2o.is_some() {
3046 return Err(syn::Error::new_spanned(
3047 field,
3048 "`ForeignKey<T>` already declares the FK target via the type parameter — \
3049 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
3050 ));
3051 }
3052 let on = attrs.on.as_deref().unwrap_or("id");
3053 return Ok(quote! {
3054 ::core::option::Option::Some(::rustango::core::Relation::Fk {
3055 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
3056 on: #on,
3057 })
3058 });
3059 }
3060 match (&attrs.fk, &attrs.o2o) {
3061 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
3062 field,
3063 "`fk` and `o2o` are mutually exclusive",
3064 )),
3065 (Some(to), None) => {
3066 let on = attrs.on.as_deref().unwrap_or("id");
3067 Ok(quote! {
3068 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #to, on: #on })
3069 })
3070 }
3071 (None, Some(to)) => {
3072 let on = attrs.on.as_deref().unwrap_or("id");
3073 Ok(quote! {
3074 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #to, on: #on })
3075 })
3076 }
3077 (None, None) => {
3078 if attrs.on.is_some() {
3079 return Err(syn::Error::new_spanned(
3080 field,
3081 "`on` requires `fk` or `o2o`",
3082 ));
3083 }
3084 Ok(quote!(::core::option::Option::None))
3085 }
3086 }
3087}
3088
3089#[derive(Clone, Copy, PartialEq, Eq)]
3093enum DetectedKind {
3094 I32,
3095 I64,
3096 F32,
3097 F64,
3098 Bool,
3099 String,
3100 DateTime,
3101 Date,
3102 Uuid,
3103 Json,
3104}
3105
3106impl DetectedKind {
3107 fn variant_tokens(self) -> TokenStream2 {
3108 match self {
3109 Self::I32 => quote!(::rustango::core::FieldType::I32),
3110 Self::I64 => quote!(::rustango::core::FieldType::I64),
3111 Self::F32 => quote!(::rustango::core::FieldType::F32),
3112 Self::F64 => quote!(::rustango::core::FieldType::F64),
3113 Self::Bool => quote!(::rustango::core::FieldType::Bool),
3114 Self::String => quote!(::rustango::core::FieldType::String),
3115 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
3116 Self::Date => quote!(::rustango::core::FieldType::Date),
3117 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
3118 Self::Json => quote!(::rustango::core::FieldType::Json),
3119 }
3120 }
3121
3122 fn is_integer(self) -> bool {
3123 matches!(self, Self::I32 | Self::I64)
3124 }
3125}
3126
3127#[derive(Clone, Copy)]
3133struct DetectedType<'a> {
3134 kind: DetectedKind,
3135 nullable: bool,
3136 auto: bool,
3137 fk_inner: Option<&'a syn::Type>,
3138}
3139
3140fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
3141 let Type::Path(TypePath { path, qself: None }) = ty else {
3142 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
3143 };
3144 let last = path
3145 .segments
3146 .last()
3147 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
3148
3149 if last.ident == "Option" {
3150 let inner = generic_inner(ty, &last.arguments, "Option")?;
3151 let inner_det = detect_type(inner)?;
3152 if inner_det.nullable {
3153 return Err(syn::Error::new_spanned(
3154 ty,
3155 "nested Option is not supported",
3156 ));
3157 }
3158 if inner_det.auto {
3159 return Err(syn::Error::new_spanned(
3160 ty,
3161 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
3162 ));
3163 }
3164 return Ok(DetectedType {
3165 nullable: true,
3166 ..inner_det
3167 });
3168 }
3169
3170 if last.ident == "Auto" {
3171 let inner = generic_inner(ty, &last.arguments, "Auto")?;
3172 let inner_det = detect_type(inner)?;
3173 if inner_det.auto {
3174 return Err(syn::Error::new_spanned(
3175 ty,
3176 "nested Auto is not supported",
3177 ));
3178 }
3179 if inner_det.nullable {
3180 return Err(syn::Error::new_spanned(
3181 ty,
3182 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
3183 ));
3184 }
3185 if inner_det.fk_inner.is_some() {
3186 return Err(syn::Error::new_spanned(
3187 ty,
3188 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
3189 ));
3190 }
3191 if !matches!(
3192 inner_det.kind,
3193 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
3194 ) {
3195 return Err(syn::Error::new_spanned(
3196 ty,
3197 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
3198 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
3199 (DEFAULT now())",
3200 ));
3201 }
3202 return Ok(DetectedType {
3203 auto: true,
3204 ..inner_det
3205 });
3206 }
3207
3208 if last.ident == "ForeignKey" {
3209 let inner = generic_inner(ty, &last.arguments, "ForeignKey")?;
3210 return Ok(DetectedType {
3215 kind: DetectedKind::I64,
3216 nullable: false,
3217 auto: false,
3218 fk_inner: Some(inner),
3219 });
3220 }
3221
3222 let kind = match last.ident.to_string().as_str() {
3223 "i32" => DetectedKind::I32,
3224 "i64" => DetectedKind::I64,
3225 "f32" => DetectedKind::F32,
3226 "f64" => DetectedKind::F64,
3227 "bool" => DetectedKind::Bool,
3228 "String" => DetectedKind::String,
3229 "DateTime" => DetectedKind::DateTime,
3230 "NaiveDate" => DetectedKind::Date,
3231 "Uuid" => DetectedKind::Uuid,
3232 "Value" => DetectedKind::Json,
3233 other => {
3234 return Err(syn::Error::new_spanned(
3235 ty,
3236 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)"),
3237 ));
3238 }
3239 };
3240 Ok(DetectedType {
3241 kind,
3242 nullable: false,
3243 auto: false,
3244 fk_inner: None,
3245 })
3246}
3247
3248fn generic_inner<'a>(
3249 ty: &'a Type,
3250 arguments: &'a PathArguments,
3251 wrapper: &str,
3252) -> syn::Result<&'a Type> {
3253 let PathArguments::AngleBracketed(args) = arguments else {
3254 return Err(syn::Error::new_spanned(
3255 ty,
3256 format!("{wrapper} requires a generic argument"),
3257 ));
3258 };
3259 args.args
3260 .iter()
3261 .find_map(|a| match a {
3262 GenericArgument::Type(t) => Some(t),
3263 _ => None,
3264 })
3265 .ok_or_else(|| {
3266 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
3267 })
3268}
3269
3270fn to_snake_case(s: &str) -> String {
3271 let mut out = String::with_capacity(s.len() + 4);
3272 for (i, ch) in s.chars().enumerate() {
3273 if ch.is_ascii_uppercase() {
3274 if i > 0 {
3275 out.push('_');
3276 }
3277 out.push(ch.to_ascii_lowercase());
3278 } else {
3279 out.push(ch);
3280 }
3281 }
3282 out
3283}
3284
3285#[derive(Default)]
3291struct FormFieldAttrs {
3292 min: Option<i64>,
3293 max: Option<i64>,
3294 min_length: Option<u32>,
3295 max_length: Option<u32>,
3296}
3297
3298#[derive(Clone, Copy)]
3300enum FormFieldKind {
3301 String,
3302 I32,
3303 I64,
3304 F32,
3305 F64,
3306 Bool,
3307}
3308
3309impl FormFieldKind {
3310 fn parse_method(self) -> &'static str {
3311 match self {
3312 Self::I32 => "i32",
3313 Self::I64 => "i64",
3314 Self::F32 => "f32",
3315 Self::F64 => "f64",
3316 Self::String | Self::Bool => "",
3319 }
3320 }
3321}
3322
3323fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
3324 let struct_name = &input.ident;
3325
3326 let Data::Struct(data) = &input.data else {
3327 return Err(syn::Error::new_spanned(
3328 struct_name,
3329 "Form can only be derived on structs",
3330 ));
3331 };
3332 let Fields::Named(named) = &data.fields else {
3333 return Err(syn::Error::new_spanned(
3334 struct_name,
3335 "Form requires a struct with named fields",
3336 ));
3337 };
3338
3339 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
3340 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
3341
3342 for field in &named.named {
3343 let ident = field
3344 .ident
3345 .as_ref()
3346 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
3347 let attrs = parse_form_field_attrs(field)?;
3348 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
3349
3350 let name_lit = ident.to_string();
3351 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
3352 field_blocks.push(parse_block);
3353 field_idents.push(ident);
3354 }
3355
3356 Ok(quote! {
3357 impl ::rustango::forms::Form for #struct_name {
3358 fn parse(
3359 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
3360 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
3361 let mut __errors = ::rustango::forms::FormErrors::default();
3362 #( #field_blocks )*
3363 if !__errors.is_empty() {
3364 return ::core::result::Result::Err(__errors);
3365 }
3366 ::core::result::Result::Ok(Self {
3367 #( #field_idents ),*
3368 })
3369 }
3370 }
3371 })
3372}
3373
3374fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
3375 let mut out = FormFieldAttrs::default();
3376 for attr in &field.attrs {
3377 if !attr.path().is_ident("form") {
3378 continue;
3379 }
3380 attr.parse_nested_meta(|meta| {
3381 if meta.path.is_ident("min") {
3382 let lit: syn::LitInt = meta.value()?.parse()?;
3383 out.min = Some(lit.base10_parse::<i64>()?);
3384 return Ok(());
3385 }
3386 if meta.path.is_ident("max") {
3387 let lit: syn::LitInt = meta.value()?.parse()?;
3388 out.max = Some(lit.base10_parse::<i64>()?);
3389 return Ok(());
3390 }
3391 if meta.path.is_ident("min_length") {
3392 let lit: syn::LitInt = meta.value()?.parse()?;
3393 out.min_length = Some(lit.base10_parse::<u32>()?);
3394 return Ok(());
3395 }
3396 if meta.path.is_ident("max_length") {
3397 let lit: syn::LitInt = meta.value()?.parse()?;
3398 out.max_length = Some(lit.base10_parse::<u32>()?);
3399 return Ok(());
3400 }
3401 Err(meta.error(
3402 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
3403 ))
3404 })?;
3405 }
3406 Ok(out)
3407}
3408
3409fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
3410 let Type::Path(TypePath { path, qself: None }) = ty else {
3411 return Err(syn::Error::new(
3412 span,
3413 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
3414 ));
3415 };
3416 let last = path
3417 .segments
3418 .last()
3419 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
3420
3421 if last.ident == "Option" {
3422 let inner = generic_inner(ty, &last.arguments, "Option")?;
3423 let (kind, nested) = detect_form_field(inner, span)?;
3424 if nested {
3425 return Err(syn::Error::new(
3426 span,
3427 "nested Option in Form fields is not supported",
3428 ));
3429 }
3430 return Ok((kind, true));
3431 }
3432
3433 let kind = match last.ident.to_string().as_str() {
3434 "String" => FormFieldKind::String,
3435 "i32" => FormFieldKind::I32,
3436 "i64" => FormFieldKind::I64,
3437 "f32" => FormFieldKind::F32,
3438 "f64" => FormFieldKind::F64,
3439 "bool" => FormFieldKind::Bool,
3440 other => {
3441 return Err(syn::Error::new(
3442 span,
3443 format!(
3444 "Form field type `{other}` is not supported in v0.8 — use String / \
3445 i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
3446 ),
3447 ));
3448 }
3449 };
3450 Ok((kind, false))
3451}
3452
3453#[allow(clippy::too_many_lines)]
3454fn render_form_field_parse(
3455 ident: &syn::Ident,
3456 name_lit: &str,
3457 kind: FormFieldKind,
3458 nullable: bool,
3459 attrs: &FormFieldAttrs,
3460) -> TokenStream2 {
3461 let lookup = quote! {
3464 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
3465 };
3466
3467 let parsed_value = match kind {
3468 FormFieldKind::Bool => quote! {
3469 let __v: bool = match __raw {
3470 ::core::option::Option::None => false,
3471 ::core::option::Option::Some(__s) => !matches!(
3472 __s.to_ascii_lowercase().as_str(),
3473 "" | "false" | "0" | "off" | "no"
3474 ),
3475 };
3476 },
3477 FormFieldKind::String => {
3478 if nullable {
3479 quote! {
3480 let __v: ::core::option::Option<::std::string::String> = match __raw {
3481 ::core::option::Option::None => ::core::option::Option::None,
3482 ::core::option::Option::Some(__s) if __s.is_empty() => {
3483 ::core::option::Option::None
3484 }
3485 ::core::option::Option::Some(__s) => {
3486 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
3487 }
3488 };
3489 }
3490 } else {
3491 quote! {
3492 let __v: ::std::string::String = match __raw {
3493 ::core::option::Option::Some(__s) if !__s.is_empty() => {
3494 ::core::clone::Clone::clone(__s)
3495 }
3496 _ => {
3497 __errors.add(#name_lit, "This field is required.");
3498 ::std::string::String::new()
3499 }
3500 };
3501 }
3502 }
3503 }
3504 FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64 => {
3505 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
3506 let ty_lit = kind.parse_method();
3507 let default_val = match kind {
3508 FormFieldKind::I32 => quote! { 0i32 },
3509 FormFieldKind::I64 => quote! { 0i64 },
3510 FormFieldKind::F32 => quote! { 0f32 },
3511 FormFieldKind::F64 => quote! { 0f64 },
3512 _ => quote! { Default::default() },
3513 };
3514 if nullable {
3515 quote! {
3516 let __v: ::core::option::Option<#parse_ty> = match __raw {
3517 ::core::option::Option::None => ::core::option::Option::None,
3518 ::core::option::Option::Some(__s) if __s.is_empty() => {
3519 ::core::option::Option::None
3520 }
3521 ::core::option::Option::Some(__s) => {
3522 match __s.parse::<#parse_ty>() {
3523 ::core::result::Result::Ok(__n) => {
3524 ::core::option::Option::Some(__n)
3525 }
3526 ::core::result::Result::Err(__e) => {
3527 __errors.add(
3528 #name_lit,
3529 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
3530 );
3531 ::core::option::Option::None
3532 }
3533 }
3534 }
3535 };
3536 }
3537 } else {
3538 quote! {
3539 let __v: #parse_ty = match __raw {
3540 ::core::option::Option::Some(__s) if !__s.is_empty() => {
3541 match __s.parse::<#parse_ty>() {
3542 ::core::result::Result::Ok(__n) => __n,
3543 ::core::result::Result::Err(__e) => {
3544 __errors.add(
3545 #name_lit,
3546 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
3547 );
3548 #default_val
3549 }
3550 }
3551 }
3552 _ => {
3553 __errors.add(#name_lit, "This field is required.");
3554 #default_val
3555 }
3556 };
3557 }
3558 }
3559 }
3560 };
3561
3562 let validators = render_form_validators(name_lit, kind, nullable, attrs);
3563
3564 quote! {
3565 let #ident = {
3566 #lookup
3567 #parsed_value
3568 #validators
3569 __v
3570 };
3571 }
3572}
3573
3574fn render_form_validators(
3575 name_lit: &str,
3576 kind: FormFieldKind,
3577 nullable: bool,
3578 attrs: &FormFieldAttrs,
3579) -> TokenStream2 {
3580 let mut checks: Vec<TokenStream2> = Vec::new();
3581
3582 let val_ref = if nullable {
3583 quote! { __v.as_ref() }
3584 } else {
3585 quote! { ::core::option::Option::Some(&__v) }
3586 };
3587
3588 let is_string = matches!(kind, FormFieldKind::String);
3589 let is_numeric = matches!(
3590 kind,
3591 FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64
3592 );
3593
3594 if is_string {
3595 if let Some(min_len) = attrs.min_length {
3596 let min_len_usize = min_len as usize;
3597 checks.push(quote! {
3598 if let ::core::option::Option::Some(__s) = #val_ref {
3599 if __s.len() < #min_len_usize {
3600 __errors.add(
3601 #name_lit,
3602 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
3603 );
3604 }
3605 }
3606 });
3607 }
3608 if let Some(max_len) = attrs.max_length {
3609 let max_len_usize = max_len as usize;
3610 checks.push(quote! {
3611 if let ::core::option::Option::Some(__s) = #val_ref {
3612 if __s.len() > #max_len_usize {
3613 __errors.add(
3614 #name_lit,
3615 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
3616 );
3617 }
3618 }
3619 });
3620 }
3621 }
3622
3623 if is_numeric {
3624 if let Some(min) = attrs.min {
3625 checks.push(quote! {
3626 if let ::core::option::Option::Some(__n) = #val_ref {
3627 if (*__n as f64) < (#min as f64) {
3628 __errors.add(
3629 #name_lit,
3630 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
3631 );
3632 }
3633 }
3634 });
3635 }
3636 if let Some(max) = attrs.max {
3637 checks.push(quote! {
3638 if let ::core::option::Option::Some(__n) = #val_ref {
3639 if (*__n as f64) > (#max as f64) {
3640 __errors.add(
3641 #name_lit,
3642 ::std::format!("Ensure this value is less than or equal to {}.", #max),
3643 );
3644 }
3645 }
3646 });
3647 }
3648 }
3649
3650 quote! { #( #checks )* }
3651}
3652
3653struct ViewSetAttrs {
3658 model: syn::Path,
3659 fields: Option<Vec<String>>,
3660 filter_fields: Vec<String>,
3661 search_fields: Vec<String>,
3662 ordering: Vec<(String, bool)>,
3664 page_size: Option<usize>,
3665 read_only: bool,
3666 perms: ViewSetPermsAttrs,
3667}
3668
3669#[derive(Default)]
3670struct ViewSetPermsAttrs {
3671 list: Vec<String>,
3672 retrieve: Vec<String>,
3673 create: Vec<String>,
3674 update: Vec<String>,
3675 destroy: Vec<String>,
3676}
3677
3678fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
3679 let struct_name = &input.ident;
3680
3681 match &input.data {
3683 Data::Struct(s) => match &s.fields {
3684 Fields::Unit | Fields::Named(_) => {}
3685 Fields::Unnamed(_) => {
3686 return Err(syn::Error::new_spanned(
3687 struct_name,
3688 "ViewSet can only be derived on a unit struct or an empty named struct",
3689 ));
3690 }
3691 },
3692 _ => {
3693 return Err(syn::Error::new_spanned(
3694 struct_name,
3695 "ViewSet can only be derived on a struct",
3696 ));
3697 }
3698 }
3699
3700 let attrs = parse_viewset_attrs(input)?;
3701 let model_path = &attrs.model;
3702
3703 let fields_call = if let Some(ref fields) = attrs.fields {
3705 let lits = fields.iter().map(|f| f.as_str());
3706 quote!(.fields(&[ #(#lits),* ]))
3707 } else {
3708 quote!()
3709 };
3710
3711 let filter_fields_call = if attrs.filter_fields.is_empty() {
3712 quote!()
3713 } else {
3714 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
3715 quote!(.filter_fields(&[ #(#lits),* ]))
3716 };
3717
3718 let search_fields_call = if attrs.search_fields.is_empty() {
3719 quote!()
3720 } else {
3721 let lits = attrs.search_fields.iter().map(|f| f.as_str());
3722 quote!(.search_fields(&[ #(#lits),* ]))
3723 };
3724
3725 let ordering_call = if attrs.ordering.is_empty() {
3726 quote!()
3727 } else {
3728 let pairs = attrs.ordering.iter().map(|(f, desc)| {
3729 let f = f.as_str();
3730 quote!((#f, #desc))
3731 });
3732 quote!(.ordering(&[ #(#pairs),* ]))
3733 };
3734
3735 let page_size_call = if let Some(n) = attrs.page_size {
3736 quote!(.page_size(#n))
3737 } else {
3738 quote!()
3739 };
3740
3741 let read_only_call = if attrs.read_only {
3742 quote!(.read_only())
3743 } else {
3744 quote!()
3745 };
3746
3747 let perms = &attrs.perms;
3748 let perms_call = if perms.list.is_empty()
3749 && perms.retrieve.is_empty()
3750 && perms.create.is_empty()
3751 && perms.update.is_empty()
3752 && perms.destroy.is_empty()
3753 {
3754 quote!()
3755 } else {
3756 let list_lits = perms.list.iter().map(|s| s.as_str());
3757 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
3758 let create_lits = perms.create.iter().map(|s| s.as_str());
3759 let update_lits = perms.update.iter().map(|s| s.as_str());
3760 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
3761 quote! {
3762 .permissions(::rustango::viewset::ViewSetPerms {
3763 list: ::std::vec![ #(#list_lits.to_owned()),* ],
3764 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
3765 create: ::std::vec![ #(#create_lits.to_owned()),* ],
3766 update: ::std::vec![ #(#update_lits.to_owned()),* ],
3767 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
3768 })
3769 }
3770 };
3771
3772 Ok(quote! {
3773 impl #struct_name {
3774 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
3777 ::rustango::viewset::ViewSet::for_model(#model_path::SCHEMA)
3778 #fields_call
3779 #filter_fields_call
3780 #search_fields_call
3781 #ordering_call
3782 #page_size_call
3783 #perms_call
3784 #read_only_call
3785 .router(prefix, pool)
3786 }
3787 }
3788 })
3789}
3790
3791fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
3792 let mut model: Option<syn::Path> = None;
3793 let mut fields: Option<Vec<String>> = None;
3794 let mut filter_fields: Vec<String> = Vec::new();
3795 let mut search_fields: Vec<String> = Vec::new();
3796 let mut ordering: Vec<(String, bool)> = Vec::new();
3797 let mut page_size: Option<usize> = None;
3798 let mut read_only = false;
3799 let mut perms = ViewSetPermsAttrs::default();
3800
3801 for attr in &input.attrs {
3802 if !attr.path().is_ident("viewset") {
3803 continue;
3804 }
3805 attr.parse_nested_meta(|meta| {
3806 if meta.path.is_ident("model") {
3807 let path: syn::Path = meta.value()?.parse()?;
3808 model = Some(path);
3809 return Ok(());
3810 }
3811 if meta.path.is_ident("fields") {
3812 let s: LitStr = meta.value()?.parse()?;
3813 fields = Some(split_field_list(&s.value()));
3814 return Ok(());
3815 }
3816 if meta.path.is_ident("filter_fields") {
3817 let s: LitStr = meta.value()?.parse()?;
3818 filter_fields = split_field_list(&s.value());
3819 return Ok(());
3820 }
3821 if meta.path.is_ident("search_fields") {
3822 let s: LitStr = meta.value()?.parse()?;
3823 search_fields = split_field_list(&s.value());
3824 return Ok(());
3825 }
3826 if meta.path.is_ident("ordering") {
3827 let s: LitStr = meta.value()?.parse()?;
3828 ordering = parse_ordering_list(&s.value());
3829 return Ok(());
3830 }
3831 if meta.path.is_ident("page_size") {
3832 let lit: syn::LitInt = meta.value()?.parse()?;
3833 page_size = Some(lit.base10_parse::<usize>()?);
3834 return Ok(());
3835 }
3836 if meta.path.is_ident("read_only") {
3837 read_only = true;
3838 return Ok(());
3839 }
3840 if meta.path.is_ident("permissions") {
3841 meta.parse_nested_meta(|inner| {
3842 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
3843 let s: LitStr = inner.value()?.parse()?;
3844 Ok(split_field_list(&s.value()))
3845 };
3846 if inner.path.is_ident("list") {
3847 perms.list = parse_codenames(&inner)?;
3848 } else if inner.path.is_ident("retrieve") {
3849 perms.retrieve = parse_codenames(&inner)?;
3850 } else if inner.path.is_ident("create") {
3851 perms.create = parse_codenames(&inner)?;
3852 } else if inner.path.is_ident("update") {
3853 perms.update = parse_codenames(&inner)?;
3854 } else if inner.path.is_ident("destroy") {
3855 perms.destroy = parse_codenames(&inner)?;
3856 } else {
3857 return Err(inner.error(
3858 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
3859 ));
3860 }
3861 Ok(())
3862 })?;
3863 return Ok(());
3864 }
3865 Err(meta.error(
3866 "unknown viewset attribute (supported: model, fields, filter_fields, \
3867 search_fields, ordering, page_size, read_only, permissions(...))",
3868 ))
3869 })?;
3870 }
3871
3872 let model = model.ok_or_else(|| {
3873 syn::Error::new_spanned(
3874 &input.ident,
3875 "`#[viewset(model = SomeModel)]` is required",
3876 )
3877 })?;
3878
3879 Ok(ViewSetAttrs {
3880 model,
3881 fields,
3882 filter_fields,
3883 search_fields,
3884 ordering,
3885 page_size,
3886 read_only,
3887 perms,
3888 })
3889}
3890
3891struct SerializerContainerAttrs {
3894 model: syn::Path,
3895}
3896
3897#[derive(Default)]
3898struct SerializerFieldAttrs {
3899 read_only: bool,
3900 write_only: bool,
3901 source: Option<String>,
3902 skip: bool,
3903}
3904
3905fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
3906 let mut model: Option<syn::Path> = None;
3907 for attr in &input.attrs {
3908 if !attr.path().is_ident("serializer") {
3909 continue;
3910 }
3911 attr.parse_nested_meta(|meta| {
3912 if meta.path.is_ident("model") {
3913 let _eq: syn::Token![=] = meta.input.parse()?;
3914 model = Some(meta.input.parse()?);
3915 return Ok(());
3916 }
3917 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
3918 })?;
3919 }
3920 let model = model.ok_or_else(|| {
3921 syn::Error::new_spanned(
3922 &input.ident,
3923 "`#[serializer(model = SomeModel)]` is required",
3924 )
3925 })?;
3926 Ok(SerializerContainerAttrs { model })
3927}
3928
3929fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
3930 let mut out = SerializerFieldAttrs::default();
3931 for attr in &field.attrs {
3932 if !attr.path().is_ident("serializer") {
3933 continue;
3934 }
3935 attr.parse_nested_meta(|meta| {
3936 if meta.path.is_ident("read_only") {
3937 out.read_only = true;
3938 return Ok(());
3939 }
3940 if meta.path.is_ident("write_only") {
3941 out.write_only = true;
3942 return Ok(());
3943 }
3944 if meta.path.is_ident("skip") {
3945 out.skip = true;
3946 return Ok(());
3947 }
3948 if meta.path.is_ident("source") {
3949 let s: LitStr = meta.value()?.parse()?;
3950 out.source = Some(s.value());
3951 return Ok(());
3952 }
3953 Err(meta.error(
3954 "unknown serializer field attribute \
3955 (supported: `read_only`, `write_only`, `source`, `skip`)",
3956 ))
3957 })?;
3958 }
3959 if out.read_only && out.write_only {
3961 return Err(syn::Error::new_spanned(
3962 field,
3963 "a field cannot be both `read_only` and `write_only`",
3964 ));
3965 }
3966 Ok(out)
3967}
3968
3969fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
3970 let struct_name = &input.ident;
3971 let struct_name_lit = struct_name.to_string();
3972
3973 let Data::Struct(data) = &input.data else {
3974 return Err(syn::Error::new_spanned(
3975 struct_name,
3976 "Serializer can only be derived on structs",
3977 ));
3978 };
3979 let Fields::Named(named) = &data.fields else {
3980 return Err(syn::Error::new_spanned(
3981 struct_name,
3982 "Serializer requires a struct with named fields",
3983 ));
3984 };
3985
3986 let container = parse_serializer_container_attrs(input)?;
3987 let model_path = &container.model;
3988
3989 #[allow(dead_code)]
3993 struct FieldInfo {
3994 ident: syn::Ident,
3995 ty: syn::Type,
3996 attrs: SerializerFieldAttrs,
3997 }
3998 let mut fields_info: Vec<FieldInfo> = Vec::new();
3999 for field in &named.named {
4000 let ident = field.ident.clone().expect("named field has ident");
4001 let attrs = parse_serializer_field_attrs(field)?;
4002 fields_info.push(FieldInfo {
4003 ident,
4004 ty: field.ty.clone(),
4005 attrs,
4006 });
4007 }
4008
4009 let from_model_fields = fields_info.iter().map(|fi| {
4011 let ident = &fi.ident;
4012 if fi.attrs.write_only || fi.attrs.skip {
4013 quote! { #ident: ::core::default::Default::default() }
4015 } else if let Some(src) = &fi.attrs.source {
4016 let src_ident = syn::Ident::new(src, ident.span());
4017 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
4018 } else {
4019 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
4020 }
4021 });
4022
4023 let output_fields: Vec<_> = fields_info
4025 .iter()
4026 .filter(|fi| !fi.attrs.write_only)
4027 .collect();
4028 let output_field_count = output_fields.len();
4029 let serialize_fields = output_fields.iter().map(|fi| {
4030 let ident = &fi.ident;
4031 let name_lit = ident.to_string();
4032 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
4033 });
4034
4035 let writable_lits: Vec<_> = fields_info
4037 .iter()
4038 .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
4039 .map(|fi| fi.ident.to_string())
4040 .collect();
4041
4042 let openapi_impl = {
4046 #[cfg(feature = "openapi")]
4047 {
4048 let property_calls = output_fields.iter().map(|fi| {
4049 let ident = &fi.ident;
4050 let name_lit = ident.to_string();
4051 let ty = &fi.ty;
4052 let nullable_call = if is_option(ty) {
4053 quote! { .nullable() }
4054 } else {
4055 quote! {}
4056 };
4057 quote! {
4058 .property(
4059 #name_lit,
4060 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
4061 #nullable_call,
4062 )
4063 }
4064 });
4065 let required_lits: Vec<_> = output_fields
4066 .iter()
4067 .filter(|fi| !is_option(&fi.ty))
4068 .map(|fi| fi.ident.to_string())
4069 .collect();
4070 quote! {
4071 impl ::rustango::openapi::OpenApiSchema for #struct_name {
4072 fn openapi_schema() -> ::rustango::openapi::Schema {
4073 ::rustango::openapi::Schema::object()
4074 #( #property_calls )*
4075 .required([ #( #required_lits ),* ])
4076 }
4077 }
4078 }
4079 }
4080 #[cfg(not(feature = "openapi"))]
4081 {
4082 quote! {}
4083 }
4084 };
4085
4086 Ok(quote! {
4087 impl ::rustango::serializer::ModelSerializer for #struct_name {
4088 type Model = #model_path;
4089
4090 fn from_model(model: &Self::Model) -> Self {
4091 Self {
4092 #( #from_model_fields ),*
4093 }
4094 }
4095
4096 fn writable_fields() -> &'static [&'static str] {
4097 &[ #( #writable_lits ),* ]
4098 }
4099 }
4100
4101 impl ::serde::Serialize for #struct_name {
4102 fn serialize<S>(&self, serializer: S)
4103 -> ::core::result::Result<S::Ok, S::Error>
4104 where
4105 S: ::serde::Serializer,
4106 {
4107 use ::serde::ser::SerializeStruct;
4108 let mut __state = serializer.serialize_struct(
4109 #struct_name_lit,
4110 #output_field_count,
4111 )?;
4112 #( #serialize_fields )*
4113 __state.end()
4114 }
4115 }
4116
4117 #openapi_impl
4118 })
4119}
4120
4121#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
4125fn is_option(ty: &syn::Type) -> bool {
4126 if let syn::Type::Path(p) = ty {
4127 if let Some(last) = p.path.segments.last() {
4128 return last.ident == "Option";
4129 }
4130 }
4131 false
4132}