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(Form, attributes(form))]
54pub fn derive_form(input: TokenStream) -> TokenStream {
55 let input = parse_macro_input!(input as DeriveInput);
56 expand_form(&input)
57 .unwrap_or_else(syn::Error::into_compile_error)
58 .into()
59}
60
61#[proc_macro]
96pub fn embed_migrations(input: TokenStream) -> TokenStream {
97 expand_embed_migrations(input.into())
98 .unwrap_or_else(syn::Error::into_compile_error)
99 .into()
100}
101
102#[proc_macro_attribute]
125pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
126 expand_main(args.into(), item.into())
127 .unwrap_or_else(syn::Error::into_compile_error)
128 .into()
129}
130
131fn expand_main(
132 args: TokenStream2,
133 item: TokenStream2,
134) -> syn::Result<TokenStream2> {
135 let mut input: syn::ItemFn = syn::parse2(item)?;
136 if input.sig.asyncness.is_none() {
137 return Err(syn::Error::new(
138 input.sig.ident.span(),
139 "`#[rustango::main]` must wrap an `async fn`",
140 ));
141 }
142
143 let tokio_attr = if args.is_empty() {
146 quote! { #[::tokio::main] }
147 } else {
148 quote! { #[::tokio::main(#args)] }
149 };
150
151 let body = input.block.clone();
153 input.block = syn::parse2(quote! {{
154 {
155 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
156 let _ = tracing_subscriber::fmt()
159 .with_env_filter(
160 EnvFilter::try_from_default_env()
161 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
162 )
163 .try_init();
164 }
165 #body
166 }})?;
167
168 Ok(quote! {
169 #tokio_attr
170 #input
171 })
172}
173
174fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
175 let path_str = if input.is_empty() {
177 "./migrations".to_string()
178 } else {
179 let lit: LitStr = syn::parse2(input)?;
180 lit.value()
181 };
182
183 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
184 syn::Error::new(
185 proc_macro2::Span::call_site(),
186 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
187 )
188 })?;
189 let abs = std::path::Path::new(&manifest).join(&path_str);
190
191 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
192 if abs.is_dir() {
193 let read = std::fs::read_dir(&abs).map_err(|e| {
194 syn::Error::new(
195 proc_macro2::Span::call_site(),
196 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
197 )
198 })?;
199 for entry in read.flatten() {
200 let path = entry.path();
201 if !path.is_file() {
202 continue;
203 }
204 if path.extension().and_then(|s| s.to_str()) != Some("json") {
205 continue;
206 }
207 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
208 continue;
209 };
210 entries.push((stem.to_owned(), path));
211 }
212 }
213 entries.sort_by(|a, b| a.0.cmp(&b.0));
214
215 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
228 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
229 for (stem, path) in &entries {
230 let raw = std::fs::read_to_string(path).map_err(|e| {
231 syn::Error::new(
232 proc_macro2::Span::call_site(),
233 format!(
234 "embed_migrations!: cannot read {} for chain validation: {e}",
235 path.display()
236 ),
237 )
238 })?;
239 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
240 syn::Error::new(
241 proc_macro2::Span::call_site(),
242 format!(
243 "embed_migrations!: {} is not valid JSON: {e}",
244 path.display()
245 ),
246 )
247 })?;
248 let name = json
249 .get("name")
250 .and_then(|v| v.as_str())
251 .ok_or_else(|| {
252 syn::Error::new(
253 proc_macro2::Span::call_site(),
254 format!(
255 "embed_migrations!: {} is missing the `name` field",
256 path.display()
257 ),
258 )
259 })?
260 .to_owned();
261 if name != *stem {
262 return Err(syn::Error::new(
263 proc_macro2::Span::call_site(),
264 format!(
265 "embed_migrations!: file stem `{stem}` does not match the migration's \
266 `name` field `{name}` — rename the file or fix the JSON",
267 ),
268 ));
269 }
270 let prev = json
271 .get("prev")
272 .and_then(|v| v.as_str())
273 .map(str::to_owned);
274 chain_names.push(name.clone());
275 prev_refs.push((name, prev));
276 }
277
278 let name_set: std::collections::HashSet<&str> =
279 chain_names.iter().map(String::as_str).collect();
280 for (name, prev) in &prev_refs {
281 if let Some(p) = prev {
282 if !name_set.contains(p.as_str()) {
283 return Err(syn::Error::new(
284 proc_macro2::Span::call_site(),
285 format!(
286 "embed_migrations!: broken migration chain — `{name}` declares \
287 prev=`{p}` but no migration with that name exists in {}",
288 abs.display()
289 ),
290 ));
291 }
292 }
293 }
294
295 let pairs: Vec<TokenStream2> = entries
296 .iter()
297 .map(|(name, path)| {
298 let path_lit = path.display().to_string();
299 quote! { (#name, ::core::include_str!(#path_lit)) }
300 })
301 .collect();
302
303 Ok(quote! {
304 {
305 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
306 __RUSTANGO_EMBEDDED
307 }
308 })
309}
310
311fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
312 let struct_name = &input.ident;
313
314 let Data::Struct(data) = &input.data else {
315 return Err(syn::Error::new_spanned(
316 struct_name,
317 "Model can only be derived on structs",
318 ));
319 };
320 let Fields::Named(named) = &data.fields else {
321 return Err(syn::Error::new_spanned(
322 struct_name,
323 "Model requires a struct with named fields",
324 ));
325 };
326
327 let container = parse_container_attrs(input)?;
328 let table = container
329 .table
330 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
331 let model_name = struct_name.to_string();
332
333 let collected = collect_fields(named)?;
334
335 if let Some((ref display, span)) = container.display {
337 if !collected.field_names.iter().any(|n| n == display) {
338 return Err(syn::Error::new(
339 span,
340 format!("`display = \"{display}\"` does not match any field on this struct"),
341 ));
342 }
343 }
344 let display = container.display.map(|(name, _)| name);
345 let app_label = container.app.clone();
346
347 if let Some(admin) = &container.admin {
349 for (label, list) in [
350 ("list_display", &admin.list_display),
351 ("search_fields", &admin.search_fields),
352 ("readonly_fields", &admin.readonly_fields),
353 ("list_filter", &admin.list_filter),
354 ] {
355 if let Some((names, span)) = list {
356 for name in names {
357 if !collected.field_names.iter().any(|n| n == name) {
358 return Err(syn::Error::new(
359 *span,
360 format!(
361 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
362 ),
363 ));
364 }
365 }
366 }
367 }
368 if let Some((pairs, span)) = &admin.ordering {
369 for (name, _) in pairs {
370 if !collected.field_names.iter().any(|n| n == name) {
371 return Err(syn::Error::new(
372 *span,
373 format!(
374 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
375 ),
376 ));
377 }
378 }
379 }
380 if let Some((groups, span)) = &admin.fieldsets {
381 for (_, fields) in groups {
382 for name in fields {
383 if !collected.field_names.iter().any(|n| n == name) {
384 return Err(syn::Error::new(
385 *span,
386 format!(
387 "`fieldsets`: \"{name}\" is not a declared field on this struct"
388 ),
389 ));
390 }
391 }
392 }
393 }
394 }
395 if let Some(audit) = &container.audit {
396 if let Some((names, span)) = &audit.track {
397 for name in names {
398 if !collected.field_names.iter().any(|n| n == name) {
399 return Err(syn::Error::new(
400 *span,
401 format!(
402 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
403 ),
404 ));
405 }
406 }
407 }
408 }
409
410 let model_impl = model_impl_tokens(
411 struct_name,
412 &model_name,
413 &table,
414 display.as_deref(),
415 app_label.as_deref(),
416 container.admin.as_ref(),
417 &collected.field_schemas,
418 collected.soft_delete_column.as_deref(),
419 );
420 let module_ident = column_module_ident(struct_name);
421 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
422 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
423 let track_set: Option<std::collections::HashSet<&str>> = audit
424 .track
425 .as_ref()
426 .map(|(names, _)| names.iter().map(String::as_str).collect());
427 collected
428 .column_entries
429 .iter()
430 .filter(|c| {
431 track_set
432 .as_ref()
433 .map_or(true, |s| s.contains(c.name.as_str()))
434 })
435 .collect()
436 });
437 let inherent_impl = inherent_impl_tokens(
438 struct_name,
439 &collected,
440 collected.primary_key.as_ref(),
441 &column_consts,
442 audited_fields.as_deref(),
443 );
444 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
445 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
446 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
447
448 Ok(quote! {
449 #model_impl
450 #inherent_impl
451 #from_row_impl
452 #column_module
453 #reverse_helpers
454
455 ::rustango::core::inventory::submit! {
456 ::rustango::core::ModelEntry {
457 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
458 module_path: ::core::module_path!(),
463 }
464 }
465 })
466}
467
468fn load_related_impl_tokens(
479 struct_name: &syn::Ident,
480 fk_relations: &[FkRelation],
481) -> TokenStream2 {
482 let arms = fk_relations.iter().map(|rel| {
483 let parent_ty = &rel.parent_type;
484 let fk_col = rel.fk_column.as_str();
485 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
488 quote! {
489 #fk_col => {
490 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
491 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
492 ::rustango::core::SqlValue::I64(v) => v,
493 _ => 0i64,
494 };
495 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
496 ::core::result::Result::Ok(true)
497 }
498 }
499 });
500 quote! {
501 impl ::rustango::sql::LoadRelated for #struct_name {
502 #[allow(unused_variables)]
503 fn __rustango_load_related(
504 &mut self,
505 row: &::rustango::sql::sqlx::postgres::PgRow,
506 field_name: &str,
507 alias: &str,
508 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
509 match field_name {
510 #( #arms )*
511 _ => ::core::result::Result::Ok(false),
512 }
513 }
514 }
515 }
516}
517
518fn fk_pk_access_impl_tokens(
526 struct_name: &syn::Ident,
527 fk_relations: &[FkRelation],
528) -> TokenStream2 {
529 let arms = fk_relations.iter().map(|rel| {
530 let fk_col = rel.fk_column.as_str();
531 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
532 quote! {
533 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
534 }
535 });
536 quote! {
537 impl ::rustango::sql::FkPkAccess for #struct_name {
538 #[allow(unused_variables)]
539 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
540 match field_name {
541 #( #arms )*
542 _ => ::core::option::Option::None,
543 }
544 }
545 }
546 }
547}
548
549fn reverse_helper_tokens(
555 child_ident: &syn::Ident,
556 fk_relations: &[FkRelation],
557) -> TokenStream2 {
558 if fk_relations.is_empty() {
559 return TokenStream2::new();
560 }
561 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
565 let method_ident = syn::Ident::new(&suffix, child_ident.span());
566 let impls = fk_relations.iter().map(|rel| {
567 let parent_ty = &rel.parent_type;
568 let fk_col = rel.fk_column.as_str();
569 let doc = format!(
570 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
571 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
572 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
573 further `{child_ident}::objects()` filters via direct queryset use."
574 );
575 quote! {
576 impl #parent_ty {
577 #[doc = #doc]
578 pub async fn #method_ident<'_c, _E>(
583 &self,
584 _executor: _E,
585 ) -> ::core::result::Result<
586 ::std::vec::Vec<#child_ident>,
587 ::rustango::sql::ExecError,
588 >
589 where
590 _E: ::rustango::sql::sqlx::Executor<
591 '_c,
592 Database = ::rustango::sql::sqlx::Postgres,
593 >,
594 {
595 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
596 ::rustango::query::QuerySet::<#child_ident>::new()
597 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
598 .fetch_on(_executor)
599 .await
600 }
601 }
602 }
603 });
604 quote! { #( #impls )* }
605}
606
607struct ColumnEntry {
608 ident: syn::Ident,
611 value_ty: Type,
613 name: String,
615 column: String,
617 field_type_tokens: TokenStream2,
619}
620
621struct CollectedFields {
622 field_schemas: Vec<TokenStream2>,
623 from_row_inits: Vec<TokenStream2>,
624 from_aliased_row_inits: Vec<TokenStream2>,
628 insert_columns: Vec<TokenStream2>,
631 insert_values: Vec<TokenStream2>,
634 insert_pushes: Vec<TokenStream2>,
639 returning_cols: Vec<TokenStream2>,
642 auto_assigns: Vec<TokenStream2>,
645 auto_field_idents: Vec<(syn::Ident, String)>,
649 bulk_pushes_no_auto: Vec<TokenStream2>,
653 bulk_pushes_all: Vec<TokenStream2>,
657 bulk_columns_no_auto: Vec<TokenStream2>,
660 bulk_columns_all: Vec<TokenStream2>,
663 bulk_auto_uniformity: Vec<TokenStream2>,
667 first_auto_ident: Option<syn::Ident>,
670 has_auto: bool,
672 pk_is_auto: bool,
676 update_assignments: Vec<TokenStream2>,
679 primary_key: Option<(syn::Ident, String)>,
680 column_entries: Vec<ColumnEntry>,
681 field_names: Vec<String>,
684 fk_relations: Vec<FkRelation>,
689 soft_delete_column: Option<String>,
694}
695
696#[derive(Clone)]
697struct FkRelation {
698 parent_type: Type,
701 fk_column: String,
704}
705
706fn collect_fields(named: &syn::FieldsNamed) -> syn::Result<CollectedFields> {
707 let cap = named.named.len();
708 let mut out = CollectedFields {
709 field_schemas: Vec::with_capacity(cap),
710 from_row_inits: Vec::with_capacity(cap),
711 from_aliased_row_inits: Vec::with_capacity(cap),
712 insert_columns: Vec::with_capacity(cap),
713 insert_values: Vec::with_capacity(cap),
714 insert_pushes: Vec::with_capacity(cap),
715 returning_cols: Vec::new(),
716 auto_assigns: Vec::new(),
717 auto_field_idents: Vec::new(),
718 bulk_pushes_no_auto: Vec::with_capacity(cap),
719 bulk_pushes_all: Vec::with_capacity(cap),
720 bulk_columns_no_auto: Vec::with_capacity(cap),
721 bulk_columns_all: Vec::with_capacity(cap),
722 bulk_auto_uniformity: Vec::new(),
723 first_auto_ident: None,
724 has_auto: false,
725 pk_is_auto: false,
726 update_assignments: Vec::with_capacity(cap),
727 primary_key: None,
728 column_entries: Vec::with_capacity(cap),
729 field_names: Vec::with_capacity(cap),
730 fk_relations: Vec::new(),
731 soft_delete_column: None,
732 };
733
734 for field in &named.named {
735 let info = process_field(field)?;
736 out.field_names.push(info.ident.to_string());
737 out.field_schemas.push(info.schema);
738 out.from_row_inits.push(info.from_row_init);
739 out.from_aliased_row_inits.push(info.from_aliased_row_init);
740 if let Some(parent_ty) = info.fk_inner.clone() {
741 out.fk_relations.push(FkRelation {
742 parent_type: parent_ty,
743 fk_column: info.column.clone(),
744 });
745 }
746 if info.soft_delete {
747 if out.soft_delete_column.is_some() {
748 return Err(syn::Error::new_spanned(
749 field,
750 "only one field may be marked `#[rustango(soft_delete)]`",
751 ));
752 }
753 out.soft_delete_column = Some(info.column.clone());
754 }
755 let column = info.column.as_str();
756 let ident = info.ident;
757 out.insert_columns.push(quote!(#column));
758 out.insert_values.push(quote! {
759 ::core::convert::Into::<::rustango::core::SqlValue>::into(
760 ::core::clone::Clone::clone(&self.#ident)
761 )
762 });
763 if info.auto {
764 out.has_auto = true;
765 if out.first_auto_ident.is_none() {
766 out.first_auto_ident = Some(ident.clone());
767 }
768 out.returning_cols.push(quote!(#column));
769 out.auto_field_idents
770 .push((ident.clone(), info.column.clone()));
771 out.auto_assigns.push(quote! {
772 self.#ident = ::rustango::sql::sqlx::Row::try_get(&_returning_row, #column)?;
773 });
774 out.insert_pushes.push(quote! {
775 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
776 _columns.push(#column);
777 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
778 ::core::clone::Clone::clone(_v)
779 ));
780 }
781 });
782 out.bulk_columns_all.push(quote!(#column));
785 out.bulk_pushes_all.push(quote! {
786 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
787 ::core::clone::Clone::clone(&_row.#ident)
788 ));
789 });
790 let ident_clone = ident.clone();
794 out.bulk_auto_uniformity.push(quote! {
795 for _r in rows.iter().skip(1) {
796 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
797 return ::core::result::Result::Err(
798 ::rustango::sql::ExecError::Sql(
799 ::rustango::sql::SqlError::BulkAutoMixed
800 )
801 );
802 }
803 }
804 });
805 } else {
806 out.insert_pushes.push(quote! {
807 _columns.push(#column);
808 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
809 ::core::clone::Clone::clone(&self.#ident)
810 ));
811 });
812 out.bulk_columns_no_auto.push(quote!(#column));
814 out.bulk_columns_all.push(quote!(#column));
815 let push_expr = quote! {
816 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
817 ::core::clone::Clone::clone(&_row.#ident)
818 ));
819 };
820 out.bulk_pushes_no_auto.push(push_expr.clone());
821 out.bulk_pushes_all.push(push_expr);
822 }
823 if info.primary_key {
824 if out.primary_key.is_some() {
825 return Err(syn::Error::new_spanned(
826 field,
827 "only one field may be marked `#[rustango(primary_key)]`",
828 ));
829 }
830 out.primary_key = Some((ident.clone(), info.column.clone()));
831 if info.auto {
832 out.pk_is_auto = true;
833 }
834 } else if info.auto_now_add {
835 } else if info.auto_now {
837 out.update_assignments.push(quote! {
842 ::rustango::core::Assignment {
843 column: #column,
844 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
845 ::chrono::Utc::now()
846 ),
847 }
848 });
849 } else {
850 out.update_assignments.push(quote! {
851 ::rustango::core::Assignment {
852 column: #column,
853 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
854 ::core::clone::Clone::clone(&self.#ident)
855 ),
856 }
857 });
858 }
859 out.column_entries.push(ColumnEntry {
860 ident: ident.clone(),
861 value_ty: info.value_ty.clone(),
862 name: ident.to_string(),
863 column: info.column.clone(),
864 field_type_tokens: info.field_type_tokens,
865 });
866 }
867 Ok(out)
868}
869
870fn model_impl_tokens(
871 struct_name: &syn::Ident,
872 model_name: &str,
873 table: &str,
874 display: Option<&str>,
875 app_label: Option<&str>,
876 admin: Option<&AdminAttrs>,
877 field_schemas: &[TokenStream2],
878 soft_delete_column: Option<&str>,
879) -> TokenStream2 {
880 let display_tokens = if let Some(name) = display {
881 quote!(::core::option::Option::Some(#name))
882 } else {
883 quote!(::core::option::Option::None)
884 };
885 let app_label_tokens = if let Some(name) = app_label {
886 quote!(::core::option::Option::Some(#name))
887 } else {
888 quote!(::core::option::Option::None)
889 };
890 let soft_delete_tokens = if let Some(col) = soft_delete_column {
891 quote!(::core::option::Option::Some(#col))
892 } else {
893 quote!(::core::option::Option::None)
894 };
895 let admin_tokens = admin_config_tokens(admin);
896 quote! {
897 impl ::rustango::core::Model for #struct_name {
898 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
899 name: #model_name,
900 table: #table,
901 fields: &[ #(#field_schemas),* ],
902 display: #display_tokens,
903 app_label: #app_label_tokens,
904 admin: #admin_tokens,
905 soft_delete_column: #soft_delete_tokens,
906 };
907 }
908 }
909}
910
911fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
915 let Some(admin) = admin else {
916 return quote!(::core::option::Option::None);
917 };
918
919 let list_display = admin
920 .list_display
921 .as_ref()
922 .map(|(v, _)| v.as_slice())
923 .unwrap_or(&[]);
924 let list_display_lits = list_display.iter().map(|s| s.as_str());
925
926 let search_fields = admin
927 .search_fields
928 .as_ref()
929 .map(|(v, _)| v.as_slice())
930 .unwrap_or(&[]);
931 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
932
933 let readonly_fields = admin
934 .readonly_fields
935 .as_ref()
936 .map(|(v, _)| v.as_slice())
937 .unwrap_or(&[]);
938 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
939
940 let list_filter = admin
941 .list_filter
942 .as_ref()
943 .map(|(v, _)| v.as_slice())
944 .unwrap_or(&[]);
945 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
946
947 let actions = admin
948 .actions
949 .as_ref()
950 .map(|(v, _)| v.as_slice())
951 .unwrap_or(&[]);
952 let actions_lits = actions.iter().map(|s| s.as_str());
953
954 let fieldsets = admin
955 .fieldsets
956 .as_ref()
957 .map(|(v, _)| v.as_slice())
958 .unwrap_or(&[]);
959 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
960 let title = title.as_str();
961 let field_lits = fields.iter().map(|s| s.as_str());
962 quote!(::rustango::core::Fieldset {
963 title: #title,
964 fields: &[ #( #field_lits ),* ],
965 })
966 });
967
968 let list_per_page = admin.list_per_page.unwrap_or(0);
969
970 let ordering_pairs = admin
971 .ordering
972 .as_ref()
973 .map(|(v, _)| v.as_slice())
974 .unwrap_or(&[]);
975 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
976 let name = name.as_str();
977 let desc = *desc;
978 quote!((#name, #desc))
979 });
980
981 quote! {
982 ::core::option::Option::Some(&::rustango::core::AdminConfig {
983 list_display: &[ #( #list_display_lits ),* ],
984 search_fields: &[ #( #search_fields_lits ),* ],
985 list_per_page: #list_per_page,
986 ordering: &[ #( #ordering_tokens ),* ],
987 readonly_fields: &[ #( #readonly_fields_lits ),* ],
988 list_filter: &[ #( #list_filter_lits ),* ],
989 actions: &[ #( #actions_lits ),* ],
990 fieldsets: &[ #( #fieldset_tokens ),* ],
991 })
992 }
993}
994
995fn inherent_impl_tokens(
996 struct_name: &syn::Ident,
997 fields: &CollectedFields,
998 primary_key: Option<&(syn::Ident, String)>,
999 column_consts: &TokenStream2,
1000 audited_fields: Option<&[&ColumnEntry]>,
1001) -> TokenStream2 {
1002 let executor_passes_to_data_write = if audited_fields.is_some() {
1008 quote!(&mut *_executor)
1009 } else {
1010 quote!(_executor)
1011 };
1012 let executor_param = if audited_fields.is_some() {
1013 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1014 } else {
1015 quote!(_executor: _E)
1016 };
1017 let executor_generics = if audited_fields.is_some() {
1018 quote!()
1019 } else {
1020 quote!(<'_c, _E>)
1021 };
1022 let executor_where = if audited_fields.is_some() {
1023 quote!()
1024 } else {
1025 quote! {
1026 where
1027 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1028 }
1029 };
1030 let pool_to_save_on = if audited_fields.is_some() {
1035 quote! {
1036 let mut _conn = pool.acquire().await?;
1037 self.save_on(&mut *_conn).await
1038 }
1039 } else {
1040 quote!(self.save_on(pool).await)
1041 };
1042 let pool_to_insert_on = if audited_fields.is_some() {
1043 quote! {
1044 let mut _conn = pool.acquire().await?;
1045 self.insert_on(&mut *_conn).await
1046 }
1047 } else {
1048 quote!(self.insert_on(pool).await)
1049 };
1050 let pool_to_delete_on = if audited_fields.is_some() {
1051 quote! {
1052 let mut _conn = pool.acquire().await?;
1053 self.delete_on(&mut *_conn).await
1054 }
1055 } else {
1056 quote!(self.delete_on(pool).await)
1057 };
1058 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1059 quote! {
1060 let mut _conn = pool.acquire().await?;
1061 Self::bulk_insert_on(rows, &mut *_conn).await
1062 }
1063 } else {
1064 quote!(Self::bulk_insert_on(rows, pool).await)
1065 };
1066
1067 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1072 .map(|tracked| {
1073 tracked
1074 .iter()
1075 .map(|c| {
1076 let column_lit = c.column.as_str();
1077 let ident = &c.ident;
1078 quote! {
1079 (
1080 #column_lit,
1081 ::serde_json::to_value(&self.#ident)
1082 .unwrap_or(::serde_json::Value::Null),
1083 )
1084 }
1085 })
1086 .collect()
1087 })
1088 .unwrap_or_default();
1089 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1090 if fields.pk_is_auto {
1091 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1092 } else {
1093 quote!(::std::format!("{}", &self.#pk_ident))
1094 }
1095 } else {
1096 quote!(::std::string::String::new())
1097 };
1098 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1099 if audited_fields.is_some() {
1100 let pairs = audit_pair_tokens.iter();
1101 let pk_str = audit_pk_to_string.clone();
1102 quote! {
1103 let _audit_entry = ::rustango::audit::PendingEntry {
1104 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1105 entity_pk: #pk_str,
1106 operation: #op_path,
1107 source: ::rustango::audit::current_source(),
1108 changes: ::rustango::audit::snapshot_changes(&[
1109 #( #pairs ),*
1110 ]),
1111 };
1112 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1113 }
1114 } else {
1115 quote!()
1116 }
1117 };
1118 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1119 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1120 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1121 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1122
1123 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) =
1133 if let Some(tracked) = audited_fields {
1134 if tracked.is_empty() {
1135 (quote!(), quote!())
1136 } else {
1137 let select_cols: String = tracked
1138 .iter()
1139 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1140 .collect::<Vec<_>>()
1141 .join(", ");
1142 let pk_column_for_select = primary_key
1143 .map(|(_, col)| col.clone())
1144 .unwrap_or_default();
1145 let select_cols_lit = select_cols;
1146 let pk_column_lit_for_select = pk_column_for_select;
1147 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
1148 if fields.pk_is_auto {
1149 quote!(self.#pk_ident.get().copied().unwrap_or_default())
1150 } else {
1151 quote!(::core::clone::Clone::clone(&self.#pk_ident))
1152 }
1153 } else {
1154 quote!(0_i64)
1155 };
1156 let before_pairs = tracked.iter().map(|c| {
1157 let column_lit = c.column.as_str();
1158 let value_ty = &c.value_ty;
1159 quote! {
1160 (
1161 #column_lit,
1162 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
1163 &_audit_before_row, #column_lit,
1164 ) {
1165 ::core::result::Result::Ok(v) => {
1166 ::serde_json::to_value(&v)
1167 .unwrap_or(::serde_json::Value::Null)
1168 }
1169 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1170 },
1171 )
1172 }
1173 });
1174 let after_pairs = tracked.iter().map(|c| {
1175 let column_lit = c.column.as_str();
1176 let ident = &c.ident;
1177 quote! {
1178 (
1179 #column_lit,
1180 ::serde_json::to_value(&self.#ident)
1181 .unwrap_or(::serde_json::Value::Null),
1182 )
1183 }
1184 });
1185 let pk_str = audit_pk_to_string.clone();
1186 let pre = quote! {
1187 let _audit_select_sql = ::std::format!(
1188 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
1189 #select_cols_lit,
1190 <Self as ::rustango::core::Model>::SCHEMA.table,
1191 #pk_column_lit_for_select,
1192 );
1193 let _audit_before_pairs:
1194 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
1195 match ::rustango::sql::sqlx::query(&_audit_select_sql)
1196 .bind(#pk_value_for_bind)
1197 .fetch_optional(&mut *_executor)
1198 .await
1199 {
1200 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
1201 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
1202 }
1203 _ => ::core::option::Option::None,
1204 };
1205 };
1206 let post = quote! {
1207 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
1208 let _audit_after:
1209 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
1210 ::std::vec![ #( #after_pairs ),* ];
1211 let _audit_entry = ::rustango::audit::PendingEntry {
1212 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1213 entity_pk: #pk_str,
1214 operation: ::rustango::audit::AuditOp::Update,
1215 source: ::rustango::audit::current_source(),
1216 changes: ::rustango::audit::diff_changes(
1217 &_audit_before,
1218 &_audit_after,
1219 ),
1220 };
1221 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1222 }
1223 };
1224 (pre, post)
1225 }
1226 } else {
1227 (quote!(), quote!())
1228 };
1229
1230 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
1234 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
1235 if fields.pk_is_auto {
1236 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1237 } else {
1238 quote!(::std::format!("{}", &_row.#pk_ident))
1239 }
1240 } else {
1241 quote!(::std::string::String::new())
1242 };
1243 let row_pairs = audited_fields
1244 .unwrap_or(&[])
1245 .iter()
1246 .map(|c| {
1247 let column_lit = c.column.as_str();
1248 let ident = &c.ident;
1249 quote! {
1250 (
1251 #column_lit,
1252 ::serde_json::to_value(&_row.#ident)
1253 .unwrap_or(::serde_json::Value::Null),
1254 )
1255 }
1256 });
1257 quote! {
1258 let _audit_source = ::rustango::audit::current_source();
1259 let mut _audit_entries:
1260 ::std::vec::Vec<::rustango::audit::PendingEntry> =
1261 ::std::vec::Vec::with_capacity(rows.len());
1262 for _row in rows.iter() {
1263 _audit_entries.push(::rustango::audit::PendingEntry {
1264 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1265 entity_pk: #row_pk_str,
1266 operation: ::rustango::audit::AuditOp::Create,
1267 source: _audit_source.clone(),
1268 changes: ::rustango::audit::snapshot_changes(&[
1269 #( #row_pairs ),*
1270 ]),
1271 });
1272 }
1273 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
1274 }
1275 } else {
1276 quote!()
1277 };
1278
1279 let save_method = if fields.pk_is_auto {
1280 let (pk_ident, pk_column) = primary_key
1281 .expect("pk_is_auto implies primary_key is Some");
1282 let pk_column_lit = pk_column.as_str();
1283 let assignments = &fields.update_assignments;
1284 Some(quote! {
1285 pub async fn save(
1303 &mut self,
1304 pool: &::rustango::sql::sqlx::PgPool,
1305 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1306 #pool_to_save_on
1307 }
1308
1309 pub async fn save_on #executor_generics (
1320 &mut self,
1321 #executor_param,
1322 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1323 #executor_where
1324 {
1325 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1326 return self.insert_on(#executor_passes_to_data_write).await;
1327 }
1328 #audit_update_pre
1329 let _query = ::rustango::core::UpdateQuery {
1330 model: <Self as ::rustango::core::Model>::SCHEMA,
1331 set: ::std::vec![ #( #assignments ),* ],
1332 where_clause: ::rustango::core::WhereExpr::Predicate(
1333 ::rustango::core::Filter {
1334 column: #pk_column_lit,
1335 op: ::rustango::core::Op::Eq,
1336 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1337 ::core::clone::Clone::clone(&self.#pk_ident)
1338 ),
1339 }
1340 ),
1341 };
1342 let _ = ::rustango::sql::update_on(
1343 #executor_passes_to_data_write,
1344 &_query,
1345 ).await?;
1346 #audit_update_post
1347 ::core::result::Result::Ok(())
1348 }
1349
1350 pub async fn save_on_with #executor_generics (
1361 &mut self,
1362 #executor_param,
1363 source: ::rustango::audit::AuditSource,
1364 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1365 #executor_where
1366 {
1367 ::rustango::audit::with_source(source, self.save_on(_executor)).await
1368 }
1369 })
1370 } else {
1371 None
1372 };
1373
1374 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
1375 let pk_column_lit = pk_column.as_str();
1376 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
1383 let col_lit = col;
1384 quote! {
1385 pub async fn soft_delete_on #executor_generics (
1395 &self,
1396 #executor_param,
1397 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1398 #executor_where
1399 {
1400 let _query = ::rustango::core::UpdateQuery {
1401 model: <Self as ::rustango::core::Model>::SCHEMA,
1402 set: ::std::vec![
1403 ::rustango::core::Assignment {
1404 column: #col_lit,
1405 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1406 ::chrono::Utc::now()
1407 ),
1408 },
1409 ],
1410 where_clause: ::rustango::core::WhereExpr::Predicate(
1411 ::rustango::core::Filter {
1412 column: #pk_column_lit,
1413 op: ::rustango::core::Op::Eq,
1414 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1415 ::core::clone::Clone::clone(&self.#pk_ident)
1416 ),
1417 }
1418 ),
1419 };
1420 let _affected = ::rustango::sql::update_on(
1421 #executor_passes_to_data_write,
1422 &_query,
1423 ).await?;
1424 #audit_softdelete_emit
1425 ::core::result::Result::Ok(_affected)
1426 }
1427
1428 pub async fn restore_on #executor_generics (
1435 &self,
1436 #executor_param,
1437 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1438 #executor_where
1439 {
1440 let _query = ::rustango::core::UpdateQuery {
1441 model: <Self as ::rustango::core::Model>::SCHEMA,
1442 set: ::std::vec![
1443 ::rustango::core::Assignment {
1444 column: #col_lit,
1445 value: ::rustango::core::SqlValue::Null,
1446 },
1447 ],
1448 where_clause: ::rustango::core::WhereExpr::Predicate(
1449 ::rustango::core::Filter {
1450 column: #pk_column_lit,
1451 op: ::rustango::core::Op::Eq,
1452 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1453 ::core::clone::Clone::clone(&self.#pk_ident)
1454 ),
1455 }
1456 ),
1457 };
1458 let _affected = ::rustango::sql::update_on(
1459 #executor_passes_to_data_write,
1460 &_query,
1461 ).await?;
1462 #audit_restore_emit
1463 ::core::result::Result::Ok(_affected)
1464 }
1465 }
1466 } else {
1467 quote!()
1468 };
1469 quote! {
1470 pub async fn delete(
1478 &self,
1479 pool: &::rustango::sql::sqlx::PgPool,
1480 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1481 #pool_to_delete_on
1482 }
1483
1484 pub async fn delete_on #executor_generics (
1491 &self,
1492 #executor_param,
1493 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1494 #executor_where
1495 {
1496 let query = ::rustango::core::DeleteQuery {
1497 model: <Self as ::rustango::core::Model>::SCHEMA,
1498 where_clause: ::rustango::core::WhereExpr::Predicate(
1499 ::rustango::core::Filter {
1500 column: #pk_column_lit,
1501 op: ::rustango::core::Op::Eq,
1502 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1503 ::core::clone::Clone::clone(&self.#pk_ident)
1504 ),
1505 }
1506 ),
1507 };
1508 let _affected = ::rustango::sql::delete_on(
1509 #executor_passes_to_data_write,
1510 &query,
1511 ).await?;
1512 #audit_delete_emit
1513 ::core::result::Result::Ok(_affected)
1514 }
1515
1516 pub async fn delete_on_with #executor_generics (
1522 &self,
1523 #executor_param,
1524 source: ::rustango::audit::AuditSource,
1525 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1526 #executor_where
1527 {
1528 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
1529 }
1530 #soft_delete_methods
1531 }
1532 });
1533
1534 let insert_method = if fields.has_auto {
1535 let pushes = &fields.insert_pushes;
1536 let returning_cols = &fields.returning_cols;
1537 let auto_assigns = &fields.auto_assigns;
1538 quote! {
1539 pub async fn insert(
1548 &mut self,
1549 pool: &::rustango::sql::sqlx::PgPool,
1550 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1551 #pool_to_insert_on
1552 }
1553
1554 pub async fn insert_on #executor_generics (
1560 &mut self,
1561 #executor_param,
1562 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1563 #executor_where
1564 {
1565 let mut _columns: ::std::vec::Vec<&'static str> =
1566 ::std::vec::Vec::new();
1567 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1568 ::std::vec::Vec::new();
1569 #( #pushes )*
1570 let query = ::rustango::core::InsertQuery {
1571 model: <Self as ::rustango::core::Model>::SCHEMA,
1572 columns: _columns,
1573 values: _values,
1574 returning: ::std::vec![ #( #returning_cols ),* ],
1575 };
1576 let _returning_row = ::rustango::sql::insert_returning_on(
1577 #executor_passes_to_data_write,
1578 &query,
1579 ).await?;
1580 #( #auto_assigns )*
1581 #audit_insert_emit
1582 ::core::result::Result::Ok(())
1583 }
1584
1585 pub async fn insert_on_with #executor_generics (
1591 &mut self,
1592 #executor_param,
1593 source: ::rustango::audit::AuditSource,
1594 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1595 #executor_where
1596 {
1597 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
1598 }
1599 }
1600 } else {
1601 let insert_columns = &fields.insert_columns;
1602 let insert_values = &fields.insert_values;
1603 quote! {
1604 pub async fn insert(
1610 &self,
1611 pool: &::rustango::sql::sqlx::PgPool,
1612 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1613 self.insert_on(pool).await
1614 }
1615
1616 pub async fn insert_on<'_c, _E>(
1622 &self,
1623 _executor: _E,
1624 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1625 where
1626 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1627 {
1628 let query = ::rustango::core::InsertQuery {
1629 model: <Self as ::rustango::core::Model>::SCHEMA,
1630 columns: ::std::vec![ #( #insert_columns ),* ],
1631 values: ::std::vec![ #( #insert_values ),* ],
1632 returning: ::std::vec::Vec::new(),
1633 };
1634 ::rustango::sql::insert_on(_executor, &query).await
1635 }
1636 }
1637 };
1638
1639 let bulk_insert_method = if fields.has_auto {
1640 let cols_no_auto = &fields.bulk_columns_no_auto;
1641 let cols_all = &fields.bulk_columns_all;
1642 let pushes_no_auto = &fields.bulk_pushes_no_auto;
1643 let pushes_all = &fields.bulk_pushes_all;
1644 let returning_cols = &fields.returning_cols;
1645 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
1646 let uniformity = &fields.bulk_auto_uniformity;
1647 let first_auto_ident = fields
1648 .first_auto_ident
1649 .as_ref()
1650 .expect("has_auto implies first_auto_ident is Some");
1651 quote! {
1652 pub async fn bulk_insert(
1666 rows: &mut [Self],
1667 pool: &::rustango::sql::sqlx::PgPool,
1668 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1669 #pool_to_bulk_insert_on
1670 }
1671
1672 pub async fn bulk_insert_on #executor_generics (
1678 rows: &mut [Self],
1679 #executor_param,
1680 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1681 #executor_where
1682 {
1683 if rows.is_empty() {
1684 return ::core::result::Result::Ok(());
1685 }
1686 let _first_unset = matches!(
1687 rows[0].#first_auto_ident,
1688 ::rustango::sql::Auto::Unset
1689 );
1690 #( #uniformity )*
1691
1692 let mut _all_rows: ::std::vec::Vec<
1693 ::std::vec::Vec<::rustango::core::SqlValue>,
1694 > = ::std::vec::Vec::with_capacity(rows.len());
1695 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
1696 for _row in rows.iter() {
1697 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1698 ::std::vec::Vec::new();
1699 #( #pushes_no_auto )*
1700 _all_rows.push(_row_vals);
1701 }
1702 ::std::vec![ #( #cols_no_auto ),* ]
1703 } else {
1704 for _row in rows.iter() {
1705 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1706 ::std::vec::Vec::new();
1707 #( #pushes_all )*
1708 _all_rows.push(_row_vals);
1709 }
1710 ::std::vec![ #( #cols_all ),* ]
1711 };
1712
1713 let _query = ::rustango::core::BulkInsertQuery {
1714 model: <Self as ::rustango::core::Model>::SCHEMA,
1715 columns: _columns,
1716 rows: _all_rows,
1717 returning: ::std::vec![ #( #returning_cols ),* ],
1718 };
1719 let _returned = ::rustango::sql::bulk_insert_on(
1720 #executor_passes_to_data_write,
1721 &_query,
1722 ).await?;
1723 if _returned.len() != rows.len() {
1724 return ::core::result::Result::Err(
1725 ::rustango::sql::ExecError::Sql(
1726 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
1727 expected: rows.len(),
1728 actual: _returned.len(),
1729 }
1730 )
1731 );
1732 }
1733 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
1734 #auto_assigns_for_row
1735 }
1736 #audit_bulk_insert_emit
1737 ::core::result::Result::Ok(())
1738 }
1739 }
1740 } else {
1741 let cols_all = &fields.bulk_columns_all;
1742 let pushes_all = &fields.bulk_pushes_all;
1743 quote! {
1744 pub async fn bulk_insert(
1754 rows: &[Self],
1755 pool: &::rustango::sql::sqlx::PgPool,
1756 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1757 Self::bulk_insert_on(rows, pool).await
1758 }
1759
1760 pub async fn bulk_insert_on<'_c, _E>(
1766 rows: &[Self],
1767 _executor: _E,
1768 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1769 where
1770 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1771 {
1772 if rows.is_empty() {
1773 return ::core::result::Result::Ok(());
1774 }
1775 let mut _all_rows: ::std::vec::Vec<
1776 ::std::vec::Vec<::rustango::core::SqlValue>,
1777 > = ::std::vec::Vec::with_capacity(rows.len());
1778 for _row in rows.iter() {
1779 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1780 ::std::vec::Vec::new();
1781 #( #pushes_all )*
1782 _all_rows.push(_row_vals);
1783 }
1784 let _query = ::rustango::core::BulkInsertQuery {
1785 model: <Self as ::rustango::core::Model>::SCHEMA,
1786 columns: ::std::vec![ #( #cols_all ),* ],
1787 rows: _all_rows,
1788 returning: ::std::vec::Vec::new(),
1789 };
1790 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
1791 ::core::result::Result::Ok(())
1792 }
1793 }
1794 };
1795
1796 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
1797 quote! {
1798 #[doc(hidden)]
1803 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
1804 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1805 ::core::clone::Clone::clone(&self.#pk_ident)
1806 )
1807 }
1808 }
1809 });
1810
1811 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
1812 quote! {
1813 impl ::rustango::sql::HasPkValue for #struct_name {
1814 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
1815 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1816 ::core::clone::Clone::clone(&self.#pk_ident)
1817 )
1818 }
1819 }
1820 }
1821 });
1822
1823 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
1824
1825 let from_aliased_row_inits = &fields.from_aliased_row_inits;
1826 let aliased_row_helper = quote! {
1827 #[doc(hidden)]
1833 pub fn __rustango_from_aliased_row(
1834 row: &::rustango::sql::sqlx::postgres::PgRow,
1835 prefix: &str,
1836 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
1837 ::core::result::Result::Ok(Self {
1838 #( #from_aliased_row_inits ),*
1839 })
1840 }
1841 };
1842
1843 let load_related_impl =
1844 load_related_impl_tokens(struct_name, &fields.fk_relations);
1845
1846 quote! {
1847 impl #struct_name {
1848 #[must_use]
1850 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
1851 ::rustango::query::QuerySet::new()
1852 }
1853
1854 #insert_method
1855
1856 #bulk_insert_method
1857
1858 #save_method
1859
1860 #pk_methods
1861
1862 #pk_value_helper
1863
1864 #aliased_row_helper
1865
1866 #column_consts
1867 }
1868
1869 #load_related_impl
1870
1871 #has_pk_value_impl
1872
1873 #fk_pk_access_impl
1874 }
1875}
1876
1877fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
1881 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
1882 let col_lit = column.as_str();
1883 quote! {
1884 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
1885 _returning_row,
1886 #col_lit,
1887 )?;
1888 }
1889 });
1890 quote! { #( #lines )* }
1891}
1892
1893fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
1895 let lines = entries.iter().map(|e| {
1896 let ident = &e.ident;
1897 let col_ty = column_type_ident(ident);
1898 quote! {
1899 #[allow(non_upper_case_globals)]
1900 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
1901 }
1902 });
1903 quote! { #(#lines)* }
1904}
1905
1906fn column_module_tokens(
1909 module_ident: &syn::Ident,
1910 struct_name: &syn::Ident,
1911 entries: &[ColumnEntry],
1912) -> TokenStream2 {
1913 let items = entries.iter().map(|e| {
1914 let col_ty = column_type_ident(&e.ident);
1915 let value_ty = &e.value_ty;
1916 let name = &e.name;
1917 let column = &e.column;
1918 let field_type_tokens = &e.field_type_tokens;
1919 quote! {
1920 #[derive(::core::clone::Clone, ::core::marker::Copy)]
1921 pub struct #col_ty;
1922
1923 impl ::rustango::core::Column for #col_ty {
1924 type Model = super::#struct_name;
1925 type Value = #value_ty;
1926 const NAME: &'static str = #name;
1927 const COLUMN: &'static str = #column;
1928 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
1929 }
1930 }
1931 });
1932 quote! {
1933 #[doc(hidden)]
1934 #[allow(non_camel_case_types, non_snake_case)]
1935 pub mod #module_ident {
1936 #[allow(unused_imports)]
1941 use super::*;
1942 #(#items)*
1943 }
1944 }
1945}
1946
1947fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
1948 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
1949}
1950
1951fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
1952 syn::Ident::new(
1953 &format!("__rustango_cols_{struct_name}"),
1954 struct_name.span(),
1955 )
1956}
1957
1958fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
1959 quote! {
1960 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
1961 for #struct_name
1962 {
1963 fn from_row(
1964 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
1965 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
1966 ::core::result::Result::Ok(Self {
1967 #( #from_row_inits ),*
1968 })
1969 }
1970 }
1971 }
1972}
1973
1974struct ContainerAttrs {
1975 table: Option<String>,
1976 display: Option<(String, proc_macro2::Span)>,
1977 app: Option<String>,
1984 admin: Option<AdminAttrs>,
1989 audit: Option<AuditAttrs>,
1995}
1996
1997#[derive(Default)]
2003struct AuditAttrs {
2004 track: Option<(Vec<String>, proc_macro2::Span)>,
2008}
2009
2010#[derive(Default)]
2015struct AdminAttrs {
2016 list_display: Option<(Vec<String>, proc_macro2::Span)>,
2017 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
2018 list_per_page: Option<usize>,
2019 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
2020 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
2021 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
2022 actions: Option<(Vec<String>, proc_macro2::Span)>,
2025 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
2029}
2030
2031fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
2032 let mut out = ContainerAttrs {
2033 table: None,
2034 display: None,
2035 app: None,
2036 admin: None,
2037 audit: None,
2038 };
2039 for attr in &input.attrs {
2040 if !attr.path().is_ident("rustango") {
2041 continue;
2042 }
2043 attr.parse_nested_meta(|meta| {
2044 if meta.path.is_ident("table") {
2045 let s: LitStr = meta.value()?.parse()?;
2046 out.table = Some(s.value());
2047 return Ok(());
2048 }
2049 if meta.path.is_ident("display") {
2050 let s: LitStr = meta.value()?.parse()?;
2051 out.display = Some((s.value(), s.span()));
2052 return Ok(());
2053 }
2054 if meta.path.is_ident("app") {
2055 let s: LitStr = meta.value()?.parse()?;
2056 out.app = Some(s.value());
2057 return Ok(());
2058 }
2059 if meta.path.is_ident("admin") {
2060 let mut admin = AdminAttrs::default();
2061 meta.parse_nested_meta(|inner| {
2062 if inner.path.is_ident("list_display") {
2063 let s: LitStr = inner.value()?.parse()?;
2064 admin.list_display =
2065 Some((split_field_list(&s.value()), s.span()));
2066 return Ok(());
2067 }
2068 if inner.path.is_ident("search_fields") {
2069 let s: LitStr = inner.value()?.parse()?;
2070 admin.search_fields =
2071 Some((split_field_list(&s.value()), s.span()));
2072 return Ok(());
2073 }
2074 if inner.path.is_ident("readonly_fields") {
2075 let s: LitStr = inner.value()?.parse()?;
2076 admin.readonly_fields =
2077 Some((split_field_list(&s.value()), s.span()));
2078 return Ok(());
2079 }
2080 if inner.path.is_ident("list_per_page") {
2081 let lit: syn::LitInt = inner.value()?.parse()?;
2082 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
2083 return Ok(());
2084 }
2085 if inner.path.is_ident("ordering") {
2086 let s: LitStr = inner.value()?.parse()?;
2087 admin.ordering = Some((
2088 parse_ordering_list(&s.value()),
2089 s.span(),
2090 ));
2091 return Ok(());
2092 }
2093 if inner.path.is_ident("list_filter") {
2094 let s: LitStr = inner.value()?.parse()?;
2095 admin.list_filter =
2096 Some((split_field_list(&s.value()), s.span()));
2097 return Ok(());
2098 }
2099 if inner.path.is_ident("actions") {
2100 let s: LitStr = inner.value()?.parse()?;
2101 admin.actions =
2102 Some((split_field_list(&s.value()), s.span()));
2103 return Ok(());
2104 }
2105 if inner.path.is_ident("fieldsets") {
2106 let s: LitStr = inner.value()?.parse()?;
2107 admin.fieldsets =
2108 Some((parse_fieldset_list(&s.value()), s.span()));
2109 return Ok(());
2110 }
2111 Err(inner.error(
2112 "unknown admin attribute (supported: \
2113 `list_display`, `search_fields`, `readonly_fields`, \
2114 `list_filter`, `list_per_page`, `ordering`, `actions`, \
2115 `fieldsets`)",
2116 ))
2117 })?;
2118 out.admin = Some(admin);
2119 return Ok(());
2120 }
2121 if meta.path.is_ident("audit") {
2122 let mut audit = AuditAttrs::default();
2123 meta.parse_nested_meta(|inner| {
2124 if inner.path.is_ident("track") {
2125 let s: LitStr = inner.value()?.parse()?;
2126 audit.track =
2127 Some((split_field_list(&s.value()), s.span()));
2128 return Ok(());
2129 }
2130 Err(inner.error(
2131 "unknown audit attribute (supported: `track`)",
2132 ))
2133 })?;
2134 out.audit = Some(audit);
2135 return Ok(());
2136 }
2137 Err(meta.error("unknown rustango container attribute"))
2138 })?;
2139 }
2140 Ok(out)
2141}
2142
2143fn split_field_list(raw: &str) -> Vec<String> {
2147 raw.split(',')
2148 .map(str::trim)
2149 .filter(|s| !s.is_empty())
2150 .map(str::to_owned)
2151 .collect()
2152}
2153
2154fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
2163 raw.split('|')
2164 .map(str::trim)
2165 .filter(|s| !s.is_empty())
2166 .map(|section| {
2167 let (title, rest) = match section.split_once(':') {
2169 Some((title, rest)) if !title.contains(',') => {
2170 (title.trim().to_owned(), rest)
2171 }
2172 _ => (String::new(), section),
2173 };
2174 let fields = split_field_list(rest);
2175 (title, fields)
2176 })
2177 .collect()
2178}
2179
2180fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
2183 raw.split(',')
2184 .map(str::trim)
2185 .filter(|s| !s.is_empty())
2186 .map(|spec| {
2187 spec.strip_prefix('-')
2188 .map_or((spec.to_owned(), false), |rest| (rest.trim().to_owned(), true))
2189 })
2190 .collect()
2191}
2192
2193struct FieldAttrs {
2194 column: Option<String>,
2195 primary_key: bool,
2196 fk: Option<String>,
2197 o2o: Option<String>,
2198 on: Option<String>,
2199 max_length: Option<u32>,
2200 min: Option<i64>,
2201 max: Option<i64>,
2202 default: Option<String>,
2203 auto_uuid: bool,
2209 auto_now_add: bool,
2214 auto_now: bool,
2220 soft_delete: bool,
2225 unique: bool,
2228}
2229
2230fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
2231 let mut out = FieldAttrs {
2232 column: None,
2233 primary_key: false,
2234 fk: None,
2235 o2o: None,
2236 on: None,
2237 max_length: None,
2238 min: None,
2239 max: None,
2240 default: None,
2241 auto_uuid: false,
2242 auto_now_add: false,
2243 auto_now: false,
2244 soft_delete: false,
2245 unique: false,
2246 };
2247 for attr in &field.attrs {
2248 if !attr.path().is_ident("rustango") {
2249 continue;
2250 }
2251 attr.parse_nested_meta(|meta| {
2252 if meta.path.is_ident("column") {
2253 let s: LitStr = meta.value()?.parse()?;
2254 out.column = Some(s.value());
2255 return Ok(());
2256 }
2257 if meta.path.is_ident("primary_key") {
2258 out.primary_key = true;
2259 return Ok(());
2260 }
2261 if meta.path.is_ident("fk") {
2262 let s: LitStr = meta.value()?.parse()?;
2263 out.fk = Some(s.value());
2264 return Ok(());
2265 }
2266 if meta.path.is_ident("o2o") {
2267 let s: LitStr = meta.value()?.parse()?;
2268 out.o2o = Some(s.value());
2269 return Ok(());
2270 }
2271 if meta.path.is_ident("on") {
2272 let s: LitStr = meta.value()?.parse()?;
2273 out.on = Some(s.value());
2274 return Ok(());
2275 }
2276 if meta.path.is_ident("max_length") {
2277 let lit: syn::LitInt = meta.value()?.parse()?;
2278 out.max_length = Some(lit.base10_parse::<u32>()?);
2279 return Ok(());
2280 }
2281 if meta.path.is_ident("min") {
2282 out.min = Some(parse_signed_i64(&meta)?);
2283 return Ok(());
2284 }
2285 if meta.path.is_ident("max") {
2286 out.max = Some(parse_signed_i64(&meta)?);
2287 return Ok(());
2288 }
2289 if meta.path.is_ident("default") {
2290 let s: LitStr = meta.value()?.parse()?;
2291 out.default = Some(s.value());
2292 return Ok(());
2293 }
2294 if meta.path.is_ident("auto_uuid") {
2295 out.auto_uuid = true;
2296 out.primary_key = true;
2300 if out.default.is_none() {
2301 out.default = Some("gen_random_uuid()".into());
2302 }
2303 return Ok(());
2304 }
2305 if meta.path.is_ident("auto_now_add") {
2306 out.auto_now_add = true;
2307 if out.default.is_none() {
2308 out.default = Some("now()".into());
2309 }
2310 return Ok(());
2311 }
2312 if meta.path.is_ident("auto_now") {
2313 out.auto_now = true;
2314 if out.default.is_none() {
2315 out.default = Some("now()".into());
2316 }
2317 return Ok(());
2318 }
2319 if meta.path.is_ident("soft_delete") {
2320 out.soft_delete = true;
2321 return Ok(());
2322 }
2323 if meta.path.is_ident("unique") {
2324 out.unique = true;
2325 return Ok(());
2326 }
2327 Err(meta.error("unknown rustango field attribute"))
2328 })?;
2329 }
2330 Ok(out)
2331}
2332
2333fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
2335 let expr: syn::Expr = meta.value()?.parse()?;
2336 match expr {
2337 syn::Expr::Lit(syn::ExprLit {
2338 lit: syn::Lit::Int(lit),
2339 ..
2340 }) => lit.base10_parse::<i64>(),
2341 syn::Expr::Unary(syn::ExprUnary {
2342 op: syn::UnOp::Neg(_),
2343 expr,
2344 ..
2345 }) => {
2346 if let syn::Expr::Lit(syn::ExprLit {
2347 lit: syn::Lit::Int(lit),
2348 ..
2349 }) = *expr
2350 {
2351 let v: i64 = lit.base10_parse()?;
2352 Ok(-v)
2353 } else {
2354 Err(syn::Error::new_spanned(expr, "expected integer literal"))
2355 }
2356 }
2357 other => Err(syn::Error::new_spanned(
2358 other,
2359 "expected integer literal (signed)",
2360 )),
2361 }
2362}
2363
2364struct FieldInfo<'a> {
2365 ident: &'a syn::Ident,
2366 column: String,
2367 primary_key: bool,
2368 auto: bool,
2372 value_ty: &'a Type,
2375 field_type_tokens: TokenStream2,
2377 schema: TokenStream2,
2378 from_row_init: TokenStream2,
2379 from_aliased_row_init: TokenStream2,
2385 fk_inner: Option<Type>,
2389 auto_now: bool,
2395 auto_now_add: bool,
2401 soft_delete: bool,
2406}
2407
2408fn process_field(field: &syn::Field) -> syn::Result<FieldInfo<'_>> {
2409 let attrs = parse_field_attrs(field)?;
2410 let ident = field
2411 .ident
2412 .as_ref()
2413 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
2414 let name = ident.to_string();
2415 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
2416 let primary_key = attrs.primary_key;
2417 let DetectedType {
2418 kind,
2419 nullable,
2420 auto: detected_auto,
2421 fk_inner,
2422 } = detect_type(&field.ty)?;
2423 check_bound_compatibility(field, &attrs, kind)?;
2424 let auto = detected_auto;
2425 if attrs.auto_uuid {
2431 if kind != DetectedKind::Uuid {
2432 return Err(syn::Error::new_spanned(
2433 field,
2434 "`#[rustango(auto_uuid)]` requires the field type to be \
2435 `Auto<uuid::Uuid>`",
2436 ));
2437 }
2438 if !detected_auto {
2439 return Err(syn::Error::new_spanned(
2440 field,
2441 "`#[rustango(auto_uuid)]` requires the field type to be \
2442 wrapped in `Auto<...>` so the macro skips the column on \
2443 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
2444 ));
2445 }
2446 }
2447 if attrs.auto_now_add || attrs.auto_now {
2448 if kind != DetectedKind::DateTime {
2449 return Err(syn::Error::new_spanned(
2450 field,
2451 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2452 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
2453 ));
2454 }
2455 if !detected_auto {
2456 return Err(syn::Error::new_spanned(
2457 field,
2458 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2459 the field type to be wrapped in `Auto<...>` so the macro skips \
2460 the column on INSERT and the DB DEFAULT (`now()`) fires",
2461 ));
2462 }
2463 }
2464 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
2465 return Err(syn::Error::new_spanned(
2466 field,
2467 "`#[rustango(soft_delete)]` requires the field type to be \
2468 `Option<chrono::DateTime<chrono::Utc>>`",
2469 ));
2470 }
2471 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
2472 if detected_auto && !primary_key && !is_mixin_auto {
2473 return Err(syn::Error::new_spanned(
2474 field,
2475 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
2476 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
2477 `auto_now`",
2478 ));
2479 }
2480 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
2481 return Err(syn::Error::new_spanned(
2482 field,
2483 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
2484 SERIAL / BIGSERIAL already supplies a default sequence.",
2485 ));
2486 }
2487 if fk_inner.is_some() && primary_key {
2488 return Err(syn::Error::new_spanned(
2489 field,
2490 "`ForeignKey<T>` is not allowed on a primary-key field — \
2491 a row's PK is its own identity, not a reference to a parent.",
2492 ));
2493 }
2494 let relation = relation_tokens(field, &attrs, fk_inner)?;
2495 let column_lit = column.as_str();
2496 let field_type_tokens = kind.variant_tokens();
2497 let max_length = optional_u32(attrs.max_length);
2498 let min = optional_i64(attrs.min);
2499 let max = optional_i64(attrs.max);
2500 let default = optional_str(attrs.default.as_deref());
2501
2502 let unique = attrs.unique;
2503 let schema = quote! {
2504 ::rustango::core::FieldSchema {
2505 name: #name,
2506 column: #column_lit,
2507 ty: #field_type_tokens,
2508 nullable: #nullable,
2509 primary_key: #primary_key,
2510 relation: #relation,
2511 max_length: #max_length,
2512 min: #min,
2513 max: #max,
2514 default: #default,
2515 auto: #auto,
2516 unique: #unique,
2517 }
2518 };
2519
2520 let from_row_init = quote! {
2521 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
2522 };
2523 let from_aliased_row_init = quote! {
2524 #ident: ::rustango::sql::sqlx::Row::try_get(
2525 row,
2526 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
2527 )?
2528 };
2529
2530 Ok(FieldInfo {
2531 ident,
2532 column,
2533 primary_key,
2534 auto,
2535 value_ty: &field.ty,
2536 field_type_tokens,
2537 schema,
2538 from_row_init,
2539 from_aliased_row_init,
2540 fk_inner: fk_inner.cloned(),
2541 auto_now: attrs.auto_now,
2542 auto_now_add: attrs.auto_now_add,
2543 soft_delete: attrs.soft_delete,
2544 })
2545}
2546
2547fn check_bound_compatibility(
2548 field: &syn::Field,
2549 attrs: &FieldAttrs,
2550 kind: DetectedKind,
2551) -> syn::Result<()> {
2552 if attrs.max_length.is_some() && kind != DetectedKind::String {
2553 return Err(syn::Error::new_spanned(
2554 field,
2555 "`max_length` is only valid on `String` fields (or `Option<String>`)",
2556 ));
2557 }
2558 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
2559 return Err(syn::Error::new_spanned(
2560 field,
2561 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
2562 ));
2563 }
2564 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
2565 if min > max {
2566 return Err(syn::Error::new_spanned(
2567 field,
2568 format!("`min` ({min}) is greater than `max` ({max})"),
2569 ));
2570 }
2571 }
2572 Ok(())
2573}
2574
2575fn optional_u32(value: Option<u32>) -> TokenStream2 {
2576 if let Some(v) = value {
2577 quote!(::core::option::Option::Some(#v))
2578 } else {
2579 quote!(::core::option::Option::None)
2580 }
2581}
2582
2583fn optional_i64(value: Option<i64>) -> TokenStream2 {
2584 if let Some(v) = value {
2585 quote!(::core::option::Option::Some(#v))
2586 } else {
2587 quote!(::core::option::Option::None)
2588 }
2589}
2590
2591fn optional_str(value: Option<&str>) -> TokenStream2 {
2592 if let Some(v) = value {
2593 quote!(::core::option::Option::Some(#v))
2594 } else {
2595 quote!(::core::option::Option::None)
2596 }
2597}
2598
2599fn relation_tokens(
2600 field: &syn::Field,
2601 attrs: &FieldAttrs,
2602 fk_inner: Option<&syn::Type>,
2603) -> syn::Result<TokenStream2> {
2604 if let Some(inner) = fk_inner {
2605 if attrs.fk.is_some() || attrs.o2o.is_some() {
2606 return Err(syn::Error::new_spanned(
2607 field,
2608 "`ForeignKey<T>` already declares the FK target via the type parameter — \
2609 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
2610 ));
2611 }
2612 let on = attrs.on.as_deref().unwrap_or("id");
2613 return Ok(quote! {
2614 ::core::option::Option::Some(::rustango::core::Relation::Fk {
2615 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
2616 on: #on,
2617 })
2618 });
2619 }
2620 match (&attrs.fk, &attrs.o2o) {
2621 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
2622 field,
2623 "`fk` and `o2o` are mutually exclusive",
2624 )),
2625 (Some(to), None) => {
2626 let on = attrs.on.as_deref().unwrap_or("id");
2627 Ok(quote! {
2628 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #to, on: #on })
2629 })
2630 }
2631 (None, Some(to)) => {
2632 let on = attrs.on.as_deref().unwrap_or("id");
2633 Ok(quote! {
2634 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #to, on: #on })
2635 })
2636 }
2637 (None, None) => {
2638 if attrs.on.is_some() {
2639 return Err(syn::Error::new_spanned(
2640 field,
2641 "`on` requires `fk` or `o2o`",
2642 ));
2643 }
2644 Ok(quote!(::core::option::Option::None))
2645 }
2646 }
2647}
2648
2649#[derive(Clone, Copy, PartialEq, Eq)]
2653enum DetectedKind {
2654 I32,
2655 I64,
2656 F32,
2657 F64,
2658 Bool,
2659 String,
2660 DateTime,
2661 Date,
2662 Uuid,
2663 Json,
2664}
2665
2666impl DetectedKind {
2667 fn variant_tokens(self) -> TokenStream2 {
2668 match self {
2669 Self::I32 => quote!(::rustango::core::FieldType::I32),
2670 Self::I64 => quote!(::rustango::core::FieldType::I64),
2671 Self::F32 => quote!(::rustango::core::FieldType::F32),
2672 Self::F64 => quote!(::rustango::core::FieldType::F64),
2673 Self::Bool => quote!(::rustango::core::FieldType::Bool),
2674 Self::String => quote!(::rustango::core::FieldType::String),
2675 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
2676 Self::Date => quote!(::rustango::core::FieldType::Date),
2677 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
2678 Self::Json => quote!(::rustango::core::FieldType::Json),
2679 }
2680 }
2681
2682 fn is_integer(self) -> bool {
2683 matches!(self, Self::I32 | Self::I64)
2684 }
2685}
2686
2687#[derive(Clone, Copy)]
2693struct DetectedType<'a> {
2694 kind: DetectedKind,
2695 nullable: bool,
2696 auto: bool,
2697 fk_inner: Option<&'a syn::Type>,
2698}
2699
2700fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
2701 let Type::Path(TypePath { path, qself: None }) = ty else {
2702 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
2703 };
2704 let last = path
2705 .segments
2706 .last()
2707 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
2708
2709 if last.ident == "Option" {
2710 let inner = generic_inner(ty, &last.arguments, "Option")?;
2711 let inner_det = detect_type(inner)?;
2712 if inner_det.nullable {
2713 return Err(syn::Error::new_spanned(
2714 ty,
2715 "nested Option is not supported",
2716 ));
2717 }
2718 if inner_det.auto {
2719 return Err(syn::Error::new_spanned(
2720 ty,
2721 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
2722 ));
2723 }
2724 return Ok(DetectedType {
2725 nullable: true,
2726 ..inner_det
2727 });
2728 }
2729
2730 if last.ident == "Auto" {
2731 let inner = generic_inner(ty, &last.arguments, "Auto")?;
2732 let inner_det = detect_type(inner)?;
2733 if inner_det.auto {
2734 return Err(syn::Error::new_spanned(
2735 ty,
2736 "nested Auto is not supported",
2737 ));
2738 }
2739 if inner_det.nullable {
2740 return Err(syn::Error::new_spanned(
2741 ty,
2742 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
2743 ));
2744 }
2745 if inner_det.fk_inner.is_some() {
2746 return Err(syn::Error::new_spanned(
2747 ty,
2748 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
2749 ));
2750 }
2751 if !matches!(
2752 inner_det.kind,
2753 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
2754 ) {
2755 return Err(syn::Error::new_spanned(
2756 ty,
2757 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
2758 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
2759 (DEFAULT now())",
2760 ));
2761 }
2762 return Ok(DetectedType {
2763 auto: true,
2764 ..inner_det
2765 });
2766 }
2767
2768 if last.ident == "ForeignKey" {
2769 let inner = generic_inner(ty, &last.arguments, "ForeignKey")?;
2770 return Ok(DetectedType {
2775 kind: DetectedKind::I64,
2776 nullable: false,
2777 auto: false,
2778 fk_inner: Some(inner),
2779 });
2780 }
2781
2782 let kind = match last.ident.to_string().as_str() {
2783 "i32" => DetectedKind::I32,
2784 "i64" => DetectedKind::I64,
2785 "f32" => DetectedKind::F32,
2786 "f64" => DetectedKind::F64,
2787 "bool" => DetectedKind::Bool,
2788 "String" => DetectedKind::String,
2789 "DateTime" => DetectedKind::DateTime,
2790 "NaiveDate" => DetectedKind::Date,
2791 "Uuid" => DetectedKind::Uuid,
2792 "Value" => DetectedKind::Json,
2793 other => {
2794 return Err(syn::Error::new_spanned(
2795 ty,
2796 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)"),
2797 ));
2798 }
2799 };
2800 Ok(DetectedType {
2801 kind,
2802 nullable: false,
2803 auto: false,
2804 fk_inner: None,
2805 })
2806}
2807
2808fn generic_inner<'a>(
2809 ty: &'a Type,
2810 arguments: &'a PathArguments,
2811 wrapper: &str,
2812) -> syn::Result<&'a Type> {
2813 let PathArguments::AngleBracketed(args) = arguments else {
2814 return Err(syn::Error::new_spanned(
2815 ty,
2816 format!("{wrapper} requires a generic argument"),
2817 ));
2818 };
2819 args.args
2820 .iter()
2821 .find_map(|a| match a {
2822 GenericArgument::Type(t) => Some(t),
2823 _ => None,
2824 })
2825 .ok_or_else(|| {
2826 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
2827 })
2828}
2829
2830fn to_snake_case(s: &str) -> String {
2831 let mut out = String::with_capacity(s.len() + 4);
2832 for (i, ch) in s.chars().enumerate() {
2833 if ch.is_ascii_uppercase() {
2834 if i > 0 {
2835 out.push('_');
2836 }
2837 out.push(ch.to_ascii_lowercase());
2838 } else {
2839 out.push(ch);
2840 }
2841 }
2842 out
2843}
2844
2845#[derive(Default)]
2851struct FormFieldAttrs {
2852 min: Option<i64>,
2853 max: Option<i64>,
2854 min_length: Option<u32>,
2855 max_length: Option<u32>,
2856}
2857
2858#[derive(Clone, Copy)]
2860enum FormFieldKind {
2861 String,
2862 I32,
2863 I64,
2864 F32,
2865 F64,
2866 Bool,
2867}
2868
2869impl FormFieldKind {
2870 fn parse_method(self) -> &'static str {
2871 match self {
2872 Self::I32 => "i32",
2873 Self::I64 => "i64",
2874 Self::F32 => "f32",
2875 Self::F64 => "f64",
2876 Self::String | Self::Bool => "",
2879 }
2880 }
2881}
2882
2883fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
2884 let struct_name = &input.ident;
2885
2886 let Data::Struct(data) = &input.data else {
2887 return Err(syn::Error::new_spanned(
2888 struct_name,
2889 "Form can only be derived on structs",
2890 ));
2891 };
2892 let Fields::Named(named) = &data.fields else {
2893 return Err(syn::Error::new_spanned(
2894 struct_name,
2895 "Form requires a struct with named fields",
2896 ));
2897 };
2898
2899 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
2900 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
2901
2902 for field in &named.named {
2903 let ident = field
2904 .ident
2905 .as_ref()
2906 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
2907 let attrs = parse_form_field_attrs(field)?;
2908 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
2909
2910 let name_lit = ident.to_string();
2911 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
2912 field_blocks.push(parse_block);
2913 field_idents.push(ident);
2914 }
2915
2916 Ok(quote! {
2917 impl ::rustango::forms::Form for #struct_name {
2918 fn parse(
2919 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
2920 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
2921 let mut __errors = ::rustango::forms::FormErrors::default();
2922 #( #field_blocks )*
2923 if !__errors.is_empty() {
2924 return ::core::result::Result::Err(__errors);
2925 }
2926 ::core::result::Result::Ok(Self {
2927 #( #field_idents ),*
2928 })
2929 }
2930 }
2931 })
2932}
2933
2934fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
2935 let mut out = FormFieldAttrs::default();
2936 for attr in &field.attrs {
2937 if !attr.path().is_ident("form") {
2938 continue;
2939 }
2940 attr.parse_nested_meta(|meta| {
2941 if meta.path.is_ident("min") {
2942 let lit: syn::LitInt = meta.value()?.parse()?;
2943 out.min = Some(lit.base10_parse::<i64>()?);
2944 return Ok(());
2945 }
2946 if meta.path.is_ident("max") {
2947 let lit: syn::LitInt = meta.value()?.parse()?;
2948 out.max = Some(lit.base10_parse::<i64>()?);
2949 return Ok(());
2950 }
2951 if meta.path.is_ident("min_length") {
2952 let lit: syn::LitInt = meta.value()?.parse()?;
2953 out.min_length = Some(lit.base10_parse::<u32>()?);
2954 return Ok(());
2955 }
2956 if meta.path.is_ident("max_length") {
2957 let lit: syn::LitInt = meta.value()?.parse()?;
2958 out.max_length = Some(lit.base10_parse::<u32>()?);
2959 return Ok(());
2960 }
2961 Err(meta.error(
2962 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
2963 ))
2964 })?;
2965 }
2966 Ok(out)
2967}
2968
2969fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
2970 let Type::Path(TypePath { path, qself: None }) = ty else {
2971 return Err(syn::Error::new(
2972 span,
2973 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
2974 ));
2975 };
2976 let last = path
2977 .segments
2978 .last()
2979 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
2980
2981 if last.ident == "Option" {
2982 let inner = generic_inner(ty, &last.arguments, "Option")?;
2983 let (kind, nested) = detect_form_field(inner, span)?;
2984 if nested {
2985 return Err(syn::Error::new(
2986 span,
2987 "nested Option in Form fields is not supported",
2988 ));
2989 }
2990 return Ok((kind, true));
2991 }
2992
2993 let kind = match last.ident.to_string().as_str() {
2994 "String" => FormFieldKind::String,
2995 "i32" => FormFieldKind::I32,
2996 "i64" => FormFieldKind::I64,
2997 "f32" => FormFieldKind::F32,
2998 "f64" => FormFieldKind::F64,
2999 "bool" => FormFieldKind::Bool,
3000 other => {
3001 return Err(syn::Error::new(
3002 span,
3003 format!(
3004 "Form field type `{other}` is not supported in v0.8 — use String / \
3005 i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
3006 ),
3007 ));
3008 }
3009 };
3010 Ok((kind, false))
3011}
3012
3013#[allow(clippy::too_many_lines)]
3014fn render_form_field_parse(
3015 ident: &syn::Ident,
3016 name_lit: &str,
3017 kind: FormFieldKind,
3018 nullable: bool,
3019 attrs: &FormFieldAttrs,
3020) -> TokenStream2 {
3021 let lookup = quote! {
3024 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
3025 };
3026
3027 let parsed_value = match kind {
3028 FormFieldKind::Bool => quote! {
3029 let __v: bool = match __raw {
3030 ::core::option::Option::None => false,
3031 ::core::option::Option::Some(__s) => !matches!(
3032 __s.to_ascii_lowercase().as_str(),
3033 "" | "false" | "0" | "off" | "no"
3034 ),
3035 };
3036 },
3037 FormFieldKind::String => {
3038 if nullable {
3039 quote! {
3040 let __v: ::core::option::Option<::std::string::String> = match __raw {
3041 ::core::option::Option::None => ::core::option::Option::None,
3042 ::core::option::Option::Some(__s) if __s.is_empty() => {
3043 ::core::option::Option::None
3044 }
3045 ::core::option::Option::Some(__s) => {
3046 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
3047 }
3048 };
3049 }
3050 } else {
3051 quote! {
3052 let __v: ::std::string::String = match __raw {
3053 ::core::option::Option::Some(__s) if !__s.is_empty() => {
3054 ::core::clone::Clone::clone(__s)
3055 }
3056 _ => {
3057 __errors.add(#name_lit, "This field is required.");
3058 ::std::string::String::new()
3059 }
3060 };
3061 }
3062 }
3063 }
3064 FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64 => {
3065 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
3066 let ty_lit = kind.parse_method();
3067 let default_val = match kind {
3068 FormFieldKind::I32 => quote! { 0i32 },
3069 FormFieldKind::I64 => quote! { 0i64 },
3070 FormFieldKind::F32 => quote! { 0f32 },
3071 FormFieldKind::F64 => quote! { 0f64 },
3072 _ => quote! { Default::default() },
3073 };
3074 if nullable {
3075 quote! {
3076 let __v: ::core::option::Option<#parse_ty> = match __raw {
3077 ::core::option::Option::None => ::core::option::Option::None,
3078 ::core::option::Option::Some(__s) if __s.is_empty() => {
3079 ::core::option::Option::None
3080 }
3081 ::core::option::Option::Some(__s) => {
3082 match __s.parse::<#parse_ty>() {
3083 ::core::result::Result::Ok(__n) => {
3084 ::core::option::Option::Some(__n)
3085 }
3086 ::core::result::Result::Err(__e) => {
3087 __errors.add(
3088 #name_lit,
3089 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
3090 );
3091 ::core::option::Option::None
3092 }
3093 }
3094 }
3095 };
3096 }
3097 } else {
3098 quote! {
3099 let __v: #parse_ty = match __raw {
3100 ::core::option::Option::Some(__s) if !__s.is_empty() => {
3101 match __s.parse::<#parse_ty>() {
3102 ::core::result::Result::Ok(__n) => __n,
3103 ::core::result::Result::Err(__e) => {
3104 __errors.add(
3105 #name_lit,
3106 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
3107 );
3108 #default_val
3109 }
3110 }
3111 }
3112 _ => {
3113 __errors.add(#name_lit, "This field is required.");
3114 #default_val
3115 }
3116 };
3117 }
3118 }
3119 }
3120 };
3121
3122 let validators = render_form_validators(name_lit, kind, nullable, attrs);
3123
3124 quote! {
3125 let #ident = {
3126 #lookup
3127 #parsed_value
3128 #validators
3129 __v
3130 };
3131 }
3132}
3133
3134fn render_form_validators(
3135 name_lit: &str,
3136 kind: FormFieldKind,
3137 nullable: bool,
3138 attrs: &FormFieldAttrs,
3139) -> TokenStream2 {
3140 let mut checks: Vec<TokenStream2> = Vec::new();
3141
3142 let val_ref = if nullable {
3143 quote! { __v.as_ref() }
3144 } else {
3145 quote! { ::core::option::Option::Some(&__v) }
3146 };
3147
3148 let is_string = matches!(kind, FormFieldKind::String);
3149 let is_numeric = matches!(
3150 kind,
3151 FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64
3152 );
3153
3154 if is_string {
3155 if let Some(min_len) = attrs.min_length {
3156 let min_len_usize = min_len as usize;
3157 checks.push(quote! {
3158 if let ::core::option::Option::Some(__s) = #val_ref {
3159 if __s.len() < #min_len_usize {
3160 __errors.add(
3161 #name_lit,
3162 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
3163 );
3164 }
3165 }
3166 });
3167 }
3168 if let Some(max_len) = attrs.max_length {
3169 let max_len_usize = max_len as usize;
3170 checks.push(quote! {
3171 if let ::core::option::Option::Some(__s) = #val_ref {
3172 if __s.len() > #max_len_usize {
3173 __errors.add(
3174 #name_lit,
3175 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
3176 );
3177 }
3178 }
3179 });
3180 }
3181 }
3182
3183 if is_numeric {
3184 if let Some(min) = attrs.min {
3185 checks.push(quote! {
3186 if let ::core::option::Option::Some(__n) = #val_ref {
3187 if (*__n as f64) < (#min as f64) {
3188 __errors.add(
3189 #name_lit,
3190 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
3191 );
3192 }
3193 }
3194 });
3195 }
3196 if let Some(max) = attrs.max {
3197 checks.push(quote! {
3198 if let ::core::option::Option::Some(__n) = #val_ref {
3199 if (*__n as f64) > (#max as f64) {
3200 __errors.add(
3201 #name_lit,
3202 ::std::format!("Ensure this value is less than or equal to {}.", #max),
3203 );
3204 }
3205 }
3206 });
3207 }
3208 }
3209
3210 quote! { #( #checks )* }
3211}