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
102fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
103 let path_str = if input.is_empty() {
105 "./migrations".to_string()
106 } else {
107 let lit: LitStr = syn::parse2(input)?;
108 lit.value()
109 };
110
111 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
112 syn::Error::new(
113 proc_macro2::Span::call_site(),
114 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
115 )
116 })?;
117 let abs = std::path::Path::new(&manifest).join(&path_str);
118
119 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
120 if abs.is_dir() {
121 let read = std::fs::read_dir(&abs).map_err(|e| {
122 syn::Error::new(
123 proc_macro2::Span::call_site(),
124 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
125 )
126 })?;
127 for entry in read.flatten() {
128 let path = entry.path();
129 if !path.is_file() {
130 continue;
131 }
132 if path.extension().and_then(|s| s.to_str()) != Some("json") {
133 continue;
134 }
135 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
136 continue;
137 };
138 entries.push((stem.to_owned(), path));
139 }
140 }
141 entries.sort_by(|a, b| a.0.cmp(&b.0));
142
143 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
156 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
157 for (stem, path) in &entries {
158 let raw = std::fs::read_to_string(path).map_err(|e| {
159 syn::Error::new(
160 proc_macro2::Span::call_site(),
161 format!(
162 "embed_migrations!: cannot read {} for chain validation: {e}",
163 path.display()
164 ),
165 )
166 })?;
167 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
168 syn::Error::new(
169 proc_macro2::Span::call_site(),
170 format!(
171 "embed_migrations!: {} is not valid JSON: {e}",
172 path.display()
173 ),
174 )
175 })?;
176 let name = json
177 .get("name")
178 .and_then(|v| v.as_str())
179 .ok_or_else(|| {
180 syn::Error::new(
181 proc_macro2::Span::call_site(),
182 format!(
183 "embed_migrations!: {} is missing the `name` field",
184 path.display()
185 ),
186 )
187 })?
188 .to_owned();
189 if name != *stem {
190 return Err(syn::Error::new(
191 proc_macro2::Span::call_site(),
192 format!(
193 "embed_migrations!: file stem `{stem}` does not match the migration's \
194 `name` field `{name}` — rename the file or fix the JSON",
195 ),
196 ));
197 }
198 let prev = json
199 .get("prev")
200 .and_then(|v| v.as_str())
201 .map(str::to_owned);
202 chain_names.push(name.clone());
203 prev_refs.push((name, prev));
204 }
205
206 let name_set: std::collections::HashSet<&str> =
207 chain_names.iter().map(String::as_str).collect();
208 for (name, prev) in &prev_refs {
209 if let Some(p) = prev {
210 if !name_set.contains(p.as_str()) {
211 return Err(syn::Error::new(
212 proc_macro2::Span::call_site(),
213 format!(
214 "embed_migrations!: broken migration chain — `{name}` declares \
215 prev=`{p}` but no migration with that name exists in {}",
216 abs.display()
217 ),
218 ));
219 }
220 }
221 }
222
223 let pairs: Vec<TokenStream2> = entries
224 .iter()
225 .map(|(name, path)| {
226 let path_lit = path.display().to_string();
227 quote! { (#name, ::core::include_str!(#path_lit)) }
228 })
229 .collect();
230
231 Ok(quote! {
232 {
233 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
234 __RUSTANGO_EMBEDDED
235 }
236 })
237}
238
239fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
240 let struct_name = &input.ident;
241
242 let Data::Struct(data) = &input.data else {
243 return Err(syn::Error::new_spanned(
244 struct_name,
245 "Model can only be derived on structs",
246 ));
247 };
248 let Fields::Named(named) = &data.fields else {
249 return Err(syn::Error::new_spanned(
250 struct_name,
251 "Model requires a struct with named fields",
252 ));
253 };
254
255 let container = parse_container_attrs(input)?;
256 let table = container
257 .table
258 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
259 let model_name = struct_name.to_string();
260
261 let collected = collect_fields(named)?;
262
263 if let Some((ref display, span)) = container.display {
265 if !collected.field_names.iter().any(|n| n == display) {
266 return Err(syn::Error::new(
267 span,
268 format!("`display = \"{display}\"` does not match any field on this struct"),
269 ));
270 }
271 }
272 let display = container.display.map(|(name, _)| name);
273
274 let model_impl = model_impl_tokens(
275 struct_name,
276 &model_name,
277 &table,
278 display.as_deref(),
279 &collected.field_schemas,
280 );
281 let module_ident = column_module_ident(struct_name);
282 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
283 let inherent_impl = inherent_impl_tokens(
284 struct_name,
285 &collected,
286 collected.primary_key.as_ref(),
287 &column_consts,
288 );
289 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
290 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
291
292 Ok(quote! {
293 #model_impl
294 #inherent_impl
295 #from_row_impl
296 #column_module
297
298 ::rustango::core::inventory::submit! {
299 ::rustango::core::ModelEntry {
300 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
301 }
302 }
303 })
304}
305
306struct ColumnEntry {
307 ident: syn::Ident,
310 value_ty: Type,
312 name: String,
314 column: String,
316 field_type_tokens: TokenStream2,
318}
319
320struct CollectedFields {
321 field_schemas: Vec<TokenStream2>,
322 from_row_inits: Vec<TokenStream2>,
323 insert_columns: Vec<TokenStream2>,
326 insert_values: Vec<TokenStream2>,
329 insert_pushes: Vec<TokenStream2>,
334 returning_cols: Vec<TokenStream2>,
337 auto_assigns: Vec<TokenStream2>,
340 auto_field_idents: Vec<(syn::Ident, String)>,
344 bulk_pushes_no_auto: Vec<TokenStream2>,
348 bulk_pushes_all: Vec<TokenStream2>,
352 bulk_columns_no_auto: Vec<TokenStream2>,
355 bulk_columns_all: Vec<TokenStream2>,
358 bulk_auto_uniformity: Vec<TokenStream2>,
362 first_auto_ident: Option<syn::Ident>,
365 has_auto: bool,
367 pk_is_auto: bool,
371 update_assignments: Vec<TokenStream2>,
374 primary_key: Option<(syn::Ident, String)>,
375 column_entries: Vec<ColumnEntry>,
376 field_names: Vec<String>,
379}
380
381fn collect_fields(named: &syn::FieldsNamed) -> syn::Result<CollectedFields> {
382 let cap = named.named.len();
383 let mut out = CollectedFields {
384 field_schemas: Vec::with_capacity(cap),
385 from_row_inits: Vec::with_capacity(cap),
386 insert_columns: Vec::with_capacity(cap),
387 insert_values: Vec::with_capacity(cap),
388 insert_pushes: Vec::with_capacity(cap),
389 returning_cols: Vec::new(),
390 auto_assigns: Vec::new(),
391 auto_field_idents: Vec::new(),
392 bulk_pushes_no_auto: Vec::with_capacity(cap),
393 bulk_pushes_all: Vec::with_capacity(cap),
394 bulk_columns_no_auto: Vec::with_capacity(cap),
395 bulk_columns_all: Vec::with_capacity(cap),
396 bulk_auto_uniformity: Vec::new(),
397 first_auto_ident: None,
398 has_auto: false,
399 pk_is_auto: false,
400 update_assignments: Vec::with_capacity(cap),
401 primary_key: None,
402 column_entries: Vec::with_capacity(cap),
403 field_names: Vec::with_capacity(cap),
404 };
405
406 for field in &named.named {
407 let info = process_field(field)?;
408 out.field_names.push(info.ident.to_string());
409 out.field_schemas.push(info.schema);
410 out.from_row_inits.push(info.from_row_init);
411 let column = info.column.as_str();
412 let ident = info.ident;
413 out.insert_columns.push(quote!(#column));
414 out.insert_values.push(quote! {
415 ::core::convert::Into::<::rustango::core::SqlValue>::into(
416 ::core::clone::Clone::clone(&self.#ident)
417 )
418 });
419 if info.auto {
420 out.has_auto = true;
421 if out.first_auto_ident.is_none() {
422 out.first_auto_ident = Some(ident.clone());
423 }
424 out.returning_cols.push(quote!(#column));
425 out.auto_field_idents
426 .push((ident.clone(), info.column.clone()));
427 out.auto_assigns.push(quote! {
428 self.#ident = ::rustango::sql::sqlx::Row::try_get(&_returning_row, #column)?;
429 });
430 out.insert_pushes.push(quote! {
431 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
432 _columns.push(#column);
433 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
434 ::core::clone::Clone::clone(_v)
435 ));
436 }
437 });
438 out.bulk_columns_all.push(quote!(#column));
441 out.bulk_pushes_all.push(quote! {
442 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
443 ::core::clone::Clone::clone(&_row.#ident)
444 ));
445 });
446 let ident_clone = ident.clone();
450 out.bulk_auto_uniformity.push(quote! {
451 for _r in rows.iter().skip(1) {
452 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
453 return ::core::result::Result::Err(
454 ::rustango::sql::ExecError::Sql(
455 ::rustango::sql::SqlError::BulkAutoMixed
456 )
457 );
458 }
459 }
460 });
461 } else {
462 out.insert_pushes.push(quote! {
463 _columns.push(#column);
464 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
465 ::core::clone::Clone::clone(&self.#ident)
466 ));
467 });
468 out.bulk_columns_no_auto.push(quote!(#column));
470 out.bulk_columns_all.push(quote!(#column));
471 let push_expr = quote! {
472 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
473 ::core::clone::Clone::clone(&_row.#ident)
474 ));
475 };
476 out.bulk_pushes_no_auto.push(push_expr.clone());
477 out.bulk_pushes_all.push(push_expr);
478 }
479 if info.primary_key {
480 if out.primary_key.is_some() {
481 return Err(syn::Error::new_spanned(
482 field,
483 "only one field may be marked `#[rustango(primary_key)]`",
484 ));
485 }
486 out.primary_key = Some((ident.clone(), info.column.clone()));
487 if info.auto {
488 out.pk_is_auto = true;
489 }
490 } else {
491 out.update_assignments.push(quote! {
492 ::rustango::core::Assignment {
493 column: #column,
494 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
495 ::core::clone::Clone::clone(&self.#ident)
496 ),
497 }
498 });
499 }
500 out.column_entries.push(ColumnEntry {
501 ident: ident.clone(),
502 value_ty: info.value_ty.clone(),
503 name: ident.to_string(),
504 column: info.column.clone(),
505 field_type_tokens: info.field_type_tokens,
506 });
507 }
508 Ok(out)
509}
510
511fn model_impl_tokens(
512 struct_name: &syn::Ident,
513 model_name: &str,
514 table: &str,
515 display: Option<&str>,
516 field_schemas: &[TokenStream2],
517) -> TokenStream2 {
518 let display_tokens = if let Some(name) = display {
519 quote!(::core::option::Option::Some(#name))
520 } else {
521 quote!(::core::option::Option::None)
522 };
523 quote! {
524 impl ::rustango::core::Model for #struct_name {
525 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
526 name: #model_name,
527 table: #table,
528 fields: &[ #(#field_schemas),* ],
529 display: #display_tokens,
530 };
531 }
532 }
533}
534
535fn inherent_impl_tokens(
536 struct_name: &syn::Ident,
537 fields: &CollectedFields,
538 primary_key: Option<&(syn::Ident, String)>,
539 column_consts: &TokenStream2,
540) -> TokenStream2 {
541 let save_method = if fields.pk_is_auto {
542 let (pk_ident, pk_column) = primary_key
543 .expect("pk_is_auto implies primary_key is Some");
544 let pk_column_lit = pk_column.as_str();
545 let assignments = &fields.update_assignments;
546 Some(quote! {
547 pub async fn save(
565 &mut self,
566 pool: &::rustango::sql::sqlx::PgPool,
567 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
568 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
569 return self.insert(pool).await;
570 }
571 let _query = ::rustango::core::UpdateQuery {
572 model: <Self as ::rustango::core::Model>::SCHEMA,
573 set: ::std::vec![ #( #assignments ),* ],
574 where_clause: ::rustango::core::WhereExpr::Predicate(
575 ::rustango::core::Filter {
576 column: #pk_column_lit,
577 op: ::rustango::core::Op::Eq,
578 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
579 ::core::clone::Clone::clone(&self.#pk_ident)
580 ),
581 }
582 ),
583 };
584 let _ = ::rustango::sql::update(pool, &_query).await?;
585 ::core::result::Result::Ok(())
586 }
587 })
588 } else {
589 None
590 };
591
592 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
593 let pk_column_lit = pk_column.as_str();
594 quote! {
595 pub async fn delete(
603 &self,
604 pool: &::rustango::sql::sqlx::PgPool,
605 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
606 let query = ::rustango::core::DeleteQuery {
607 model: <Self as ::rustango::core::Model>::SCHEMA,
608 where_clause: ::rustango::core::WhereExpr::Predicate(
609 ::rustango::core::Filter {
610 column: #pk_column_lit,
611 op: ::rustango::core::Op::Eq,
612 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
613 ::core::clone::Clone::clone(&self.#pk_ident)
614 ),
615 }
616 ),
617 };
618 ::rustango::sql::delete(pool, &query).await
619 }
620 }
621 });
622
623 let insert_method = if fields.has_auto {
624 let pushes = &fields.insert_pushes;
625 let returning_cols = &fields.returning_cols;
626 let auto_assigns = &fields.auto_assigns;
627 quote! {
628 pub async fn insert(
637 &mut self,
638 pool: &::rustango::sql::sqlx::PgPool,
639 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
640 let mut _columns: ::std::vec::Vec<&'static str> =
641 ::std::vec::Vec::new();
642 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
643 ::std::vec::Vec::new();
644 #( #pushes )*
645 let query = ::rustango::core::InsertQuery {
646 model: <Self as ::rustango::core::Model>::SCHEMA,
647 columns: _columns,
648 values: _values,
649 returning: ::std::vec![ #( #returning_cols ),* ],
650 };
651 let _returning_row = ::rustango::sql::insert_returning(pool, &query).await?;
652 #( #auto_assigns )*
653 ::core::result::Result::Ok(())
654 }
655 }
656 } else {
657 let insert_columns = &fields.insert_columns;
658 let insert_values = &fields.insert_values;
659 quote! {
660 pub async fn insert(
666 &self,
667 pool: &::rustango::sql::sqlx::PgPool,
668 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
669 let query = ::rustango::core::InsertQuery {
670 model: <Self as ::rustango::core::Model>::SCHEMA,
671 columns: ::std::vec![ #( #insert_columns ),* ],
672 values: ::std::vec![ #( #insert_values ),* ],
673 returning: ::std::vec::Vec::new(),
674 };
675 ::rustango::sql::insert(pool, &query).await
676 }
677 }
678 };
679
680 let bulk_insert_method = if fields.has_auto {
681 let cols_no_auto = &fields.bulk_columns_no_auto;
682 let cols_all = &fields.bulk_columns_all;
683 let pushes_no_auto = &fields.bulk_pushes_no_auto;
684 let pushes_all = &fields.bulk_pushes_all;
685 let returning_cols = &fields.returning_cols;
686 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
687 let uniformity = &fields.bulk_auto_uniformity;
688 let first_auto_ident = fields
689 .first_auto_ident
690 .as_ref()
691 .expect("has_auto implies first_auto_ident is Some");
692 quote! {
693 pub async fn bulk_insert(
707 rows: &mut [Self],
708 pool: &::rustango::sql::sqlx::PgPool,
709 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
710 if rows.is_empty() {
711 return ::core::result::Result::Ok(());
712 }
713 let _first_unset = matches!(
714 rows[0].#first_auto_ident,
715 ::rustango::sql::Auto::Unset
716 );
717 #( #uniformity )*
718
719 let mut _all_rows: ::std::vec::Vec<
720 ::std::vec::Vec<::rustango::core::SqlValue>,
721 > = ::std::vec::Vec::with_capacity(rows.len());
722 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
723 for _row in rows.iter() {
724 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
725 ::std::vec::Vec::new();
726 #( #pushes_no_auto )*
727 _all_rows.push(_row_vals);
728 }
729 ::std::vec![ #( #cols_no_auto ),* ]
730 } else {
731 for _row in rows.iter() {
732 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
733 ::std::vec::Vec::new();
734 #( #pushes_all )*
735 _all_rows.push(_row_vals);
736 }
737 ::std::vec![ #( #cols_all ),* ]
738 };
739
740 let _query = ::rustango::core::BulkInsertQuery {
741 model: <Self as ::rustango::core::Model>::SCHEMA,
742 columns: _columns,
743 rows: _all_rows,
744 returning: ::std::vec![ #( #returning_cols ),* ],
745 };
746 let _returned = ::rustango::sql::bulk_insert(pool, &_query).await?;
747 if _returned.len() != rows.len() {
748 return ::core::result::Result::Err(
749 ::rustango::sql::ExecError::Sql(
750 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
751 expected: rows.len(),
752 actual: _returned.len(),
753 }
754 )
755 );
756 }
757 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
758 #auto_assigns_for_row
759 }
760 ::core::result::Result::Ok(())
761 }
762 }
763 } else {
764 let cols_all = &fields.bulk_columns_all;
765 let pushes_all = &fields.bulk_pushes_all;
766 quote! {
767 pub async fn bulk_insert(
777 rows: &[Self],
778 pool: &::rustango::sql::sqlx::PgPool,
779 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
780 if rows.is_empty() {
781 return ::core::result::Result::Ok(());
782 }
783 let mut _all_rows: ::std::vec::Vec<
784 ::std::vec::Vec<::rustango::core::SqlValue>,
785 > = ::std::vec::Vec::with_capacity(rows.len());
786 for _row in rows.iter() {
787 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
788 ::std::vec::Vec::new();
789 #( #pushes_all )*
790 _all_rows.push(_row_vals);
791 }
792 let _query = ::rustango::core::BulkInsertQuery {
793 model: <Self as ::rustango::core::Model>::SCHEMA,
794 columns: ::std::vec![ #( #cols_all ),* ],
795 rows: _all_rows,
796 returning: ::std::vec::Vec::new(),
797 };
798 let _ = ::rustango::sql::bulk_insert(pool, &_query).await?;
799 ::core::result::Result::Ok(())
800 }
801 }
802 };
803
804 quote! {
805 impl #struct_name {
806 #[must_use]
808 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
809 ::rustango::query::QuerySet::new()
810 }
811
812 #insert_method
813
814 #bulk_insert_method
815
816 #save_method
817
818 #pk_methods
819
820 #column_consts
821 }
822 }
823}
824
825fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
829 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
830 let col_lit = column.as_str();
831 quote! {
832 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
833 _returning_row,
834 #col_lit,
835 )?;
836 }
837 });
838 quote! { #( #lines )* }
839}
840
841fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
843 let lines = entries.iter().map(|e| {
844 let ident = &e.ident;
845 let col_ty = column_type_ident(ident);
846 quote! {
847 #[allow(non_upper_case_globals)]
848 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
849 }
850 });
851 quote! { #(#lines)* }
852}
853
854fn column_module_tokens(
857 module_ident: &syn::Ident,
858 struct_name: &syn::Ident,
859 entries: &[ColumnEntry],
860) -> TokenStream2 {
861 let items = entries.iter().map(|e| {
862 let col_ty = column_type_ident(&e.ident);
863 let value_ty = &e.value_ty;
864 let name = &e.name;
865 let column = &e.column;
866 let field_type_tokens = &e.field_type_tokens;
867 quote! {
868 #[derive(::core::clone::Clone, ::core::marker::Copy)]
869 pub struct #col_ty;
870
871 impl ::rustango::core::Column for #col_ty {
872 type Model = super::#struct_name;
873 type Value = #value_ty;
874 const NAME: &'static str = #name;
875 const COLUMN: &'static str = #column;
876 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
877 }
878 }
879 });
880 quote! {
881 #[doc(hidden)]
882 #[allow(non_camel_case_types, non_snake_case)]
883 pub mod #module_ident {
884 #[allow(unused_imports)]
889 use super::*;
890 #(#items)*
891 }
892 }
893}
894
895fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
896 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
897}
898
899fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
900 syn::Ident::new(
901 &format!("__rustango_cols_{struct_name}"),
902 struct_name.span(),
903 )
904}
905
906fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
907 quote! {
908 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
909 for #struct_name
910 {
911 fn from_row(
912 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
913 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
914 ::core::result::Result::Ok(Self {
915 #( #from_row_inits ),*
916 })
917 }
918 }
919 }
920}
921
922struct ContainerAttrs {
923 table: Option<String>,
924 display: Option<(String, proc_macro2::Span)>,
925}
926
927fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
928 let mut out = ContainerAttrs {
929 table: None,
930 display: None,
931 };
932 for attr in &input.attrs {
933 if !attr.path().is_ident("rustango") {
934 continue;
935 }
936 attr.parse_nested_meta(|meta| {
937 if meta.path.is_ident("table") {
938 let s: LitStr = meta.value()?.parse()?;
939 out.table = Some(s.value());
940 return Ok(());
941 }
942 if meta.path.is_ident("display") {
943 let s: LitStr = meta.value()?.parse()?;
944 out.display = Some((s.value(), s.span()));
945 return Ok(());
946 }
947 Err(meta.error("unknown rustango container attribute"))
948 })?;
949 }
950 Ok(out)
951}
952
953struct FieldAttrs {
954 column: Option<String>,
955 primary_key: bool,
956 fk: Option<String>,
957 o2o: Option<String>,
958 on: Option<String>,
959 max_length: Option<u32>,
960 min: Option<i64>,
961 max: Option<i64>,
962 default: Option<String>,
963}
964
965fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
966 let mut out = FieldAttrs {
967 column: None,
968 primary_key: false,
969 fk: None,
970 o2o: None,
971 on: None,
972 max_length: None,
973 min: None,
974 max: None,
975 default: None,
976 };
977 for attr in &field.attrs {
978 if !attr.path().is_ident("rustango") {
979 continue;
980 }
981 attr.parse_nested_meta(|meta| {
982 if meta.path.is_ident("column") {
983 let s: LitStr = meta.value()?.parse()?;
984 out.column = Some(s.value());
985 return Ok(());
986 }
987 if meta.path.is_ident("primary_key") {
988 out.primary_key = true;
989 return Ok(());
990 }
991 if meta.path.is_ident("fk") {
992 let s: LitStr = meta.value()?.parse()?;
993 out.fk = Some(s.value());
994 return Ok(());
995 }
996 if meta.path.is_ident("o2o") {
997 let s: LitStr = meta.value()?.parse()?;
998 out.o2o = Some(s.value());
999 return Ok(());
1000 }
1001 if meta.path.is_ident("on") {
1002 let s: LitStr = meta.value()?.parse()?;
1003 out.on = Some(s.value());
1004 return Ok(());
1005 }
1006 if meta.path.is_ident("max_length") {
1007 let lit: syn::LitInt = meta.value()?.parse()?;
1008 out.max_length = Some(lit.base10_parse::<u32>()?);
1009 return Ok(());
1010 }
1011 if meta.path.is_ident("min") {
1012 out.min = Some(parse_signed_i64(&meta)?);
1013 return Ok(());
1014 }
1015 if meta.path.is_ident("max") {
1016 out.max = Some(parse_signed_i64(&meta)?);
1017 return Ok(());
1018 }
1019 if meta.path.is_ident("default") {
1020 let s: LitStr = meta.value()?.parse()?;
1021 out.default = Some(s.value());
1022 return Ok(());
1023 }
1024 Err(meta.error("unknown rustango field attribute"))
1025 })?;
1026 }
1027 Ok(out)
1028}
1029
1030fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
1032 let expr: syn::Expr = meta.value()?.parse()?;
1033 match expr {
1034 syn::Expr::Lit(syn::ExprLit {
1035 lit: syn::Lit::Int(lit),
1036 ..
1037 }) => lit.base10_parse::<i64>(),
1038 syn::Expr::Unary(syn::ExprUnary {
1039 op: syn::UnOp::Neg(_),
1040 expr,
1041 ..
1042 }) => {
1043 if let syn::Expr::Lit(syn::ExprLit {
1044 lit: syn::Lit::Int(lit),
1045 ..
1046 }) = *expr
1047 {
1048 let v: i64 = lit.base10_parse()?;
1049 Ok(-v)
1050 } else {
1051 Err(syn::Error::new_spanned(expr, "expected integer literal"))
1052 }
1053 }
1054 other => Err(syn::Error::new_spanned(
1055 other,
1056 "expected integer literal (signed)",
1057 )),
1058 }
1059}
1060
1061struct FieldInfo<'a> {
1062 ident: &'a syn::Ident,
1063 column: String,
1064 primary_key: bool,
1065 auto: bool,
1069 value_ty: &'a Type,
1072 field_type_tokens: TokenStream2,
1074 schema: TokenStream2,
1075 from_row_init: TokenStream2,
1076}
1077
1078fn process_field(field: &syn::Field) -> syn::Result<FieldInfo<'_>> {
1079 let attrs = parse_field_attrs(field)?;
1080 let ident = field
1081 .ident
1082 .as_ref()
1083 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
1084 let name = ident.to_string();
1085 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
1086 let primary_key = attrs.primary_key;
1087 let DetectedType {
1088 kind,
1089 nullable,
1090 auto,
1091 fk_inner,
1092 } = detect_type(&field.ty)?;
1093 check_bound_compatibility(field, &attrs, kind)?;
1094 if auto && !primary_key {
1095 return Err(syn::Error::new_spanned(
1096 field,
1097 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field",
1098 ));
1099 }
1100 if auto && attrs.default.is_some() {
1101 return Err(syn::Error::new_spanned(
1102 field,
1103 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
1104 SERIAL / BIGSERIAL already supplies a default sequence.",
1105 ));
1106 }
1107 if fk_inner.is_some() && primary_key {
1108 return Err(syn::Error::new_spanned(
1109 field,
1110 "`ForeignKey<T>` is not allowed on a primary-key field — \
1111 a row's PK is its own identity, not a reference to a parent.",
1112 ));
1113 }
1114 let relation = relation_tokens(field, &attrs, fk_inner)?;
1115 let column_lit = column.as_str();
1116 let field_type_tokens = kind.variant_tokens();
1117 let max_length = optional_u32(attrs.max_length);
1118 let min = optional_i64(attrs.min);
1119 let max = optional_i64(attrs.max);
1120 let default = optional_str(attrs.default.as_deref());
1121
1122 let schema = quote! {
1123 ::rustango::core::FieldSchema {
1124 name: #name,
1125 column: #column_lit,
1126 ty: #field_type_tokens,
1127 nullable: #nullable,
1128 primary_key: #primary_key,
1129 relation: #relation,
1130 max_length: #max_length,
1131 min: #min,
1132 max: #max,
1133 default: #default,
1134 auto: #auto,
1135 }
1136 };
1137
1138 let from_row_init = quote! {
1139 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
1140 };
1141
1142 Ok(FieldInfo {
1143 ident,
1144 column,
1145 primary_key,
1146 auto,
1147 value_ty: &field.ty,
1148 field_type_tokens,
1149 schema,
1150 from_row_init,
1151 })
1152}
1153
1154fn check_bound_compatibility(
1155 field: &syn::Field,
1156 attrs: &FieldAttrs,
1157 kind: DetectedKind,
1158) -> syn::Result<()> {
1159 if attrs.max_length.is_some() && kind != DetectedKind::String {
1160 return Err(syn::Error::new_spanned(
1161 field,
1162 "`max_length` is only valid on `String` fields (or `Option<String>`)",
1163 ));
1164 }
1165 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
1166 return Err(syn::Error::new_spanned(
1167 field,
1168 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
1169 ));
1170 }
1171 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
1172 if min > max {
1173 return Err(syn::Error::new_spanned(
1174 field,
1175 format!("`min` ({min}) is greater than `max` ({max})"),
1176 ));
1177 }
1178 }
1179 Ok(())
1180}
1181
1182fn optional_u32(value: Option<u32>) -> TokenStream2 {
1183 if let Some(v) = value {
1184 quote!(::core::option::Option::Some(#v))
1185 } else {
1186 quote!(::core::option::Option::None)
1187 }
1188}
1189
1190fn optional_i64(value: Option<i64>) -> TokenStream2 {
1191 if let Some(v) = value {
1192 quote!(::core::option::Option::Some(#v))
1193 } else {
1194 quote!(::core::option::Option::None)
1195 }
1196}
1197
1198fn optional_str(value: Option<&str>) -> TokenStream2 {
1199 if let Some(v) = value {
1200 quote!(::core::option::Option::Some(#v))
1201 } else {
1202 quote!(::core::option::Option::None)
1203 }
1204}
1205
1206fn relation_tokens(
1207 field: &syn::Field,
1208 attrs: &FieldAttrs,
1209 fk_inner: Option<&syn::Type>,
1210) -> syn::Result<TokenStream2> {
1211 if let Some(inner) = fk_inner {
1212 if attrs.fk.is_some() || attrs.o2o.is_some() {
1213 return Err(syn::Error::new_spanned(
1214 field,
1215 "`ForeignKey<T>` already declares the FK target via the type parameter — \
1216 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
1217 ));
1218 }
1219 let on = attrs.on.as_deref().unwrap_or("id");
1220 return Ok(quote! {
1221 ::core::option::Option::Some(::rustango::core::Relation::Fk {
1222 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
1223 on: #on,
1224 })
1225 });
1226 }
1227 match (&attrs.fk, &attrs.o2o) {
1228 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
1229 field,
1230 "`fk` and `o2o` are mutually exclusive",
1231 )),
1232 (Some(to), None) => {
1233 let on = attrs.on.as_deref().unwrap_or("id");
1234 Ok(quote! {
1235 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #to, on: #on })
1236 })
1237 }
1238 (None, Some(to)) => {
1239 let on = attrs.on.as_deref().unwrap_or("id");
1240 Ok(quote! {
1241 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #to, on: #on })
1242 })
1243 }
1244 (None, None) => {
1245 if attrs.on.is_some() {
1246 return Err(syn::Error::new_spanned(
1247 field,
1248 "`on` requires `fk` or `o2o`",
1249 ));
1250 }
1251 Ok(quote!(::core::option::Option::None))
1252 }
1253 }
1254}
1255
1256#[derive(Clone, Copy, PartialEq, Eq)]
1260enum DetectedKind {
1261 I32,
1262 I64,
1263 F32,
1264 F64,
1265 Bool,
1266 String,
1267 DateTime,
1268 Date,
1269 Uuid,
1270 Json,
1271}
1272
1273impl DetectedKind {
1274 fn variant_tokens(self) -> TokenStream2 {
1275 match self {
1276 Self::I32 => quote!(::rustango::core::FieldType::I32),
1277 Self::I64 => quote!(::rustango::core::FieldType::I64),
1278 Self::F32 => quote!(::rustango::core::FieldType::F32),
1279 Self::F64 => quote!(::rustango::core::FieldType::F64),
1280 Self::Bool => quote!(::rustango::core::FieldType::Bool),
1281 Self::String => quote!(::rustango::core::FieldType::String),
1282 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
1283 Self::Date => quote!(::rustango::core::FieldType::Date),
1284 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
1285 Self::Json => quote!(::rustango::core::FieldType::Json),
1286 }
1287 }
1288
1289 fn is_integer(self) -> bool {
1290 matches!(self, Self::I32 | Self::I64)
1291 }
1292}
1293
1294#[derive(Clone, Copy)]
1300struct DetectedType<'a> {
1301 kind: DetectedKind,
1302 nullable: bool,
1303 auto: bool,
1304 fk_inner: Option<&'a syn::Type>,
1305}
1306
1307fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
1308 let Type::Path(TypePath { path, qself: None }) = ty else {
1309 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
1310 };
1311 let last = path
1312 .segments
1313 .last()
1314 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
1315
1316 if last.ident == "Option" {
1317 let inner = generic_inner(ty, &last.arguments, "Option")?;
1318 let inner_det = detect_type(inner)?;
1319 if inner_det.nullable {
1320 return Err(syn::Error::new_spanned(
1321 ty,
1322 "nested Option is not supported",
1323 ));
1324 }
1325 if inner_det.auto {
1326 return Err(syn::Error::new_spanned(
1327 ty,
1328 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
1329 ));
1330 }
1331 return Ok(DetectedType {
1332 nullable: true,
1333 ..inner_det
1334 });
1335 }
1336
1337 if last.ident == "Auto" {
1338 let inner = generic_inner(ty, &last.arguments, "Auto")?;
1339 let inner_det = detect_type(inner)?;
1340 if inner_det.auto {
1341 return Err(syn::Error::new_spanned(
1342 ty,
1343 "nested Auto is not supported",
1344 ));
1345 }
1346 if inner_det.nullable {
1347 return Err(syn::Error::new_spanned(
1348 ty,
1349 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
1350 ));
1351 }
1352 if inner_det.fk_inner.is_some() {
1353 return Err(syn::Error::new_spanned(
1354 ty,
1355 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
1356 ));
1357 }
1358 if !matches!(inner_det.kind, DetectedKind::I32 | DetectedKind::I64) {
1359 return Err(syn::Error::new_spanned(
1360 ty,
1361 "`Auto<T>` only supports integer types (`i32` → SERIAL, `i64` → BIGSERIAL)",
1362 ));
1363 }
1364 return Ok(DetectedType {
1365 auto: true,
1366 ..inner_det
1367 });
1368 }
1369
1370 if last.ident == "ForeignKey" {
1371 let inner = generic_inner(ty, &last.arguments, "ForeignKey")?;
1372 return Ok(DetectedType {
1377 kind: DetectedKind::I64,
1378 nullable: false,
1379 auto: false,
1380 fk_inner: Some(inner),
1381 });
1382 }
1383
1384 let kind = match last.ident.to_string().as_str() {
1385 "i32" => DetectedKind::I32,
1386 "i64" => DetectedKind::I64,
1387 "f32" => DetectedKind::F32,
1388 "f64" => DetectedKind::F64,
1389 "bool" => DetectedKind::Bool,
1390 "String" => DetectedKind::String,
1391 "DateTime" => DetectedKind::DateTime,
1392 "NaiveDate" => DetectedKind::Date,
1393 "Uuid" => DetectedKind::Uuid,
1394 "Value" => DetectedKind::Json,
1395 other => {
1396 return Err(syn::Error::new_spanned(
1397 ty,
1398 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)"),
1399 ));
1400 }
1401 };
1402 Ok(DetectedType {
1403 kind,
1404 nullable: false,
1405 auto: false,
1406 fk_inner: None,
1407 })
1408}
1409
1410fn generic_inner<'a>(
1411 ty: &'a Type,
1412 arguments: &'a PathArguments,
1413 wrapper: &str,
1414) -> syn::Result<&'a Type> {
1415 let PathArguments::AngleBracketed(args) = arguments else {
1416 return Err(syn::Error::new_spanned(
1417 ty,
1418 format!("{wrapper} requires a generic argument"),
1419 ));
1420 };
1421 args.args
1422 .iter()
1423 .find_map(|a| match a {
1424 GenericArgument::Type(t) => Some(t),
1425 _ => None,
1426 })
1427 .ok_or_else(|| {
1428 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
1429 })
1430}
1431
1432fn to_snake_case(s: &str) -> String {
1433 let mut out = String::with_capacity(s.len() + 4);
1434 for (i, ch) in s.chars().enumerate() {
1435 if ch.is_ascii_uppercase() {
1436 if i > 0 {
1437 out.push('_');
1438 }
1439 out.push(ch.to_ascii_lowercase());
1440 } else {
1441 out.push(ch);
1442 }
1443 }
1444 out
1445}
1446
1447#[derive(Default)]
1453struct FormFieldAttrs {
1454 min: Option<i64>,
1455 max: Option<i64>,
1456 min_length: Option<u32>,
1457 max_length: Option<u32>,
1458}
1459
1460#[derive(Clone, Copy)]
1462enum FormFieldKind {
1463 String,
1464 I32,
1465 I64,
1466 F32,
1467 F64,
1468 Bool,
1469}
1470
1471impl FormFieldKind {
1472 fn parse_method(self) -> &'static str {
1473 match self {
1474 Self::I32 => "i32",
1475 Self::I64 => "i64",
1476 Self::F32 => "f32",
1477 Self::F64 => "f64",
1478 Self::String | Self::Bool => "",
1481 }
1482 }
1483}
1484
1485fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
1486 let struct_name = &input.ident;
1487
1488 let Data::Struct(data) = &input.data else {
1489 return Err(syn::Error::new_spanned(
1490 struct_name,
1491 "Form can only be derived on structs",
1492 ));
1493 };
1494 let Fields::Named(named) = &data.fields else {
1495 return Err(syn::Error::new_spanned(
1496 struct_name,
1497 "Form requires a struct with named fields",
1498 ));
1499 };
1500
1501 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
1502 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
1503
1504 for field in &named.named {
1505 let ident = field
1506 .ident
1507 .as_ref()
1508 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
1509 let attrs = parse_form_field_attrs(field)?;
1510 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
1511
1512 let name_lit = ident.to_string();
1513 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
1514 field_blocks.push(parse_block);
1515 field_idents.push(ident);
1516 }
1517
1518 Ok(quote! {
1519 impl ::rustango::forms::FormStruct for #struct_name {
1520 fn parse(
1521 form: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
1522 ) -> ::core::result::Result<Self, ::rustango::forms::FormError> {
1523 #( #field_blocks )*
1524 ::core::result::Result::Ok(Self {
1525 #( #field_idents ),*
1526 })
1527 }
1528 }
1529 })
1530}
1531
1532fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
1533 let mut out = FormFieldAttrs::default();
1534 for attr in &field.attrs {
1535 if !attr.path().is_ident("form") {
1536 continue;
1537 }
1538 attr.parse_nested_meta(|meta| {
1539 if meta.path.is_ident("min") {
1540 let lit: syn::LitInt = meta.value()?.parse()?;
1541 out.min = Some(lit.base10_parse::<i64>()?);
1542 return Ok(());
1543 }
1544 if meta.path.is_ident("max") {
1545 let lit: syn::LitInt = meta.value()?.parse()?;
1546 out.max = Some(lit.base10_parse::<i64>()?);
1547 return Ok(());
1548 }
1549 if meta.path.is_ident("min_length") {
1550 let lit: syn::LitInt = meta.value()?.parse()?;
1551 out.min_length = Some(lit.base10_parse::<u32>()?);
1552 return Ok(());
1553 }
1554 if meta.path.is_ident("max_length") {
1555 let lit: syn::LitInt = meta.value()?.parse()?;
1556 out.max_length = Some(lit.base10_parse::<u32>()?);
1557 return Ok(());
1558 }
1559 Err(meta.error(
1560 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
1561 ))
1562 })?;
1563 }
1564 Ok(out)
1565}
1566
1567fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
1568 let Type::Path(TypePath { path, qself: None }) = ty else {
1569 return Err(syn::Error::new(
1570 span,
1571 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
1572 ));
1573 };
1574 let last = path
1575 .segments
1576 .last()
1577 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
1578
1579 if last.ident == "Option" {
1580 let inner = generic_inner(ty, &last.arguments, "Option")?;
1581 let (kind, nested) = detect_form_field(inner, span)?;
1582 if nested {
1583 return Err(syn::Error::new(
1584 span,
1585 "nested Option in Form fields is not supported",
1586 ));
1587 }
1588 return Ok((kind, true));
1589 }
1590
1591 let kind = match last.ident.to_string().as_str() {
1592 "String" => FormFieldKind::String,
1593 "i32" => FormFieldKind::I32,
1594 "i64" => FormFieldKind::I64,
1595 "f32" => FormFieldKind::F32,
1596 "f64" => FormFieldKind::F64,
1597 "bool" => FormFieldKind::Bool,
1598 other => {
1599 return Err(syn::Error::new(
1600 span,
1601 format!(
1602 "Form field type `{other}` is not supported in v0.8 — use String / \
1603 i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
1604 ),
1605 ));
1606 }
1607 };
1608 Ok((kind, false))
1609}
1610
1611#[allow(clippy::too_many_lines)]
1612fn render_form_field_parse(
1613 ident: &syn::Ident,
1614 name_lit: &str,
1615 kind: FormFieldKind,
1616 nullable: bool,
1617 attrs: &FormFieldAttrs,
1618) -> TokenStream2 {
1619 let lookup = quote! {
1624 let __raw: ::core::option::Option<&::std::string::String> = form.get(#name_lit);
1625 };
1626
1627 let parsed_value = match kind {
1628 FormFieldKind::Bool => quote! {
1629 let __v: bool = match __raw {
1632 ::core::option::Option::None => false,
1633 ::core::option::Option::Some(__s) => !matches!(
1634 __s.to_ascii_lowercase().as_str(),
1635 "" | "false" | "0" | "off" | "no"
1636 ),
1637 };
1638 },
1639 FormFieldKind::String => {
1640 if nullable {
1641 quote! {
1642 let __v: ::core::option::Option<::std::string::String> = match __raw {
1643 ::core::option::Option::None => ::core::option::Option::None,
1644 ::core::option::Option::Some(__s) if __s.is_empty() => {
1645 ::core::option::Option::None
1646 }
1647 ::core::option::Option::Some(__s) => {
1648 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
1649 }
1650 };
1651 }
1652 } else {
1653 quote! {
1654 let __v: ::std::string::String = match __raw {
1655 ::core::option::Option::Some(__s) if !__s.is_empty() => {
1656 ::core::clone::Clone::clone(__s)
1657 }
1658 _ => {
1659 return ::core::result::Result::Err(
1660 ::rustango::forms::FormError::Missing {
1661 field: ::std::string::String::from(#name_lit),
1662 }
1663 );
1664 }
1665 };
1666 }
1667 }
1668 }
1669 FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64 => {
1670 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
1671 let ty_lit = kind.parse_method();
1672 let parse_expr = quote! {
1673 __s.parse::<#parse_ty>().map_err(|__e| {
1674 ::rustango::forms::FormError::Parse {
1675 field: ::std::string::String::from(#name_lit),
1676 ty: #ty_lit,
1677 value: ::core::clone::Clone::clone(__s),
1678 detail: ::std::string::ToString::to_string(&__e),
1679 }
1680 })
1681 };
1682 if nullable {
1683 quote! {
1684 let __v: ::core::option::Option<#parse_ty> = match __raw {
1685 ::core::option::Option::None => ::core::option::Option::None,
1686 ::core::option::Option::Some(__s) if __s.is_empty() => {
1687 ::core::option::Option::None
1688 }
1689 ::core::option::Option::Some(__s) => {
1690 ::core::option::Option::Some(#parse_expr?)
1691 }
1692 };
1693 }
1694 } else {
1695 quote! {
1696 let __v: #parse_ty = match __raw {
1697 ::core::option::Option::Some(__s) if !__s.is_empty() => {
1698 #parse_expr?
1699 }
1700 _ => {
1701 return ::core::result::Result::Err(
1702 ::rustango::forms::FormError::Missing {
1703 field: ::std::string::String::from(#name_lit),
1704 }
1705 );
1706 }
1707 };
1708 }
1709 }
1710 }
1711 };
1712
1713 let validators = render_form_validators(name_lit, kind, nullable, attrs);
1718
1719 quote! {
1720 let #ident = {
1721 #lookup
1722 #parsed_value
1723 #validators
1724 __v
1725 };
1726 }
1727}
1728
1729fn render_form_validators(
1730 name_lit: &str,
1731 kind: FormFieldKind,
1732 nullable: bool,
1733 attrs: &FormFieldAttrs,
1734) -> TokenStream2 {
1735 let mut checks: Vec<TokenStream2> = Vec::new();
1736
1737 let val_ref = if nullable {
1738 quote! { __v.as_ref() }
1740 } else {
1741 quote! { ::core::option::Option::Some(&__v) }
1742 };
1743
1744 let is_string = matches!(kind, FormFieldKind::String);
1745 let is_numeric = matches!(
1746 kind,
1747 FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64
1748 );
1749
1750 if is_string {
1751 if let Some(min_len) = attrs.min_length {
1752 let min_len_usize = min_len as usize;
1753 checks.push(quote! {
1754 if let ::core::option::Option::Some(__s) = #val_ref {
1755 if __s.len() < #min_len_usize {
1756 return ::core::result::Result::Err(
1757 ::rustango::forms::FormError::Parse {
1758 field: ::std::string::String::from(#name_lit),
1759 ty: "String",
1760 value: ::core::clone::Clone::clone(__s),
1761 detail: ::std::format!(
1762 "shorter than min_length {}", #min_len_usize
1763 ),
1764 }
1765 );
1766 }
1767 }
1768 });
1769 }
1770 if let Some(max_len) = attrs.max_length {
1771 let max_len_usize = max_len as usize;
1772 checks.push(quote! {
1773 if let ::core::option::Option::Some(__s) = #val_ref {
1774 if __s.len() > #max_len_usize {
1775 return ::core::result::Result::Err(
1776 ::rustango::forms::FormError::Parse {
1777 field: ::std::string::String::from(#name_lit),
1778 ty: "String",
1779 value: ::core::clone::Clone::clone(__s),
1780 detail: ::std::format!(
1781 "longer than max_length {}", #max_len_usize
1782 ),
1783 }
1784 );
1785 }
1786 }
1787 });
1788 }
1789 }
1790
1791 if is_numeric {
1792 if let Some(min) = attrs.min {
1793 checks.push(quote! {
1794 if let ::core::option::Option::Some(__n) = #val_ref {
1795 let __nf = (*__n) as f64;
1796 if __nf < (#min as f64) {
1797 return ::core::result::Result::Err(
1798 ::rustango::forms::FormError::Parse {
1799 field: ::std::string::String::from(#name_lit),
1800 ty: "numeric",
1801 value: ::std::string::ToString::to_string(__n),
1802 detail: ::std::format!("less than min {}", #min),
1803 }
1804 );
1805 }
1806 }
1807 });
1808 }
1809 if let Some(max) = attrs.max {
1810 checks.push(quote! {
1811 if let ::core::option::Option::Some(__n) = #val_ref {
1812 let __nf = (*__n) as f64;
1813 if __nf > (#max as f64) {
1814 return ::core::result::Result::Err(
1815 ::rustango::forms::FormError::Parse {
1816 field: ::std::string::String::from(#name_lit),
1817 ty: "numeric",
1818 value: ::std::string::ToString::to_string(__n),
1819 detail: ::std::format!("greater than max {}", #max),
1820 }
1821 );
1822 }
1823 }
1824 });
1825 }
1826 }
1827
1828 quote! { #( #checks )* }
1829}