Skip to main content

rustango_macros/
lib.rs

1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use 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/// Derive a `Model` impl. See crate docs for the supported attributes.
18#[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/// Derive `rustango::forms::FormStruct` (slice 8.4B). Generates a
27/// `parse(&HashMap<String, String>) -> Result<Self, FormError>` impl
28/// that walks every named field and:
29///
30/// * Parses the string value into the field's Rust type (`String`,
31///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
32///   nullable case).
33/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
34///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
35///   validators in declaration order, returning `FormError::Parse`
36///   on the first failure.
37///
38/// Example:
39///
40/// ```ignore
41/// #[derive(Form)]
42/// pub struct CreateItemForm {
43///     #[form(min_length = 1, max_length = 64)]
44///     pub name: String,
45///     #[form(min = 0, max = 150)]
46///     pub age: i32,
47///     pub active: bool,
48///     pub email: Option<String>,
49/// }
50///
51/// let parsed = CreateItemForm::parse(&form_map)?;
52/// ```
53#[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/// Bake every `*.json` migration file in a directory into the binary
62/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
63/// of `(name, json_content)` pairs, lex-sorted by file stem.
64///
65/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
66/// behaviour as `migrate(pool, dir)` but with no filesystem access.
67/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
68/// (i.e. the crate that invokes the macro). Default is
69/// `"./migrations"` if no argument is supplied.
70///
71/// ```ignore
72/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
73/// // or:
74/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
75///
76/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
77/// ```
78///
79/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
80/// file's `name` field must equal its file stem, every `prev`
81/// reference must point to another migration in the same directory,
82/// and the JSON must parse. A broken chain — orphan `prev`, missing
83/// predecessor, malformed file — fails at macro-expansion time with
84/// a clear `compile_error!`. *No other Django-shape Rust framework
85/// validates migration chains at compile time*: Cot's migrations are
86/// imperative Rust code (no static chain), Loco's are SeaORM
87/// up/down (same), Rwf's are raw SQL (no chain at all).
88///
89/// Each migration is included via `include_str!` so cargo's rebuild
90/// detection picks up file *content* changes. **Caveat:** cargo
91/// doesn't watch directory listings, so adding or removing a
92/// migration file inside the dir won't auto-trigger a rebuild — run
93/// `cargo clean` (or just bump any other source file) when you add
94/// new migrations during embedded development.
95#[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/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
103/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
104/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
105/// functions are zero-boilerplate:
106///
107/// ```ignore
108/// #[rustango::main]
109/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
110///     rustango::server::Builder::from_env().await?
111///         .migrate("migrations").await?
112///         .api(my_app::urls::api())
113///         .seed_with(my_app::seed::run).await?
114///         .serve("0.0.0.0:8080").await
115/// }
116/// ```
117///
118/// Optional `flavor = "current_thread"` passes through to
119/// `#[tokio::main]`; default is the multi-threaded runtime.
120///
121/// Pulls `tracing-subscriber` into the rustango crate behind the
122/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
123/// out get plain `#[tokio::main]` ergonomics without the dependency.
124#[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    // Parse optional `flavor = "..."` etc. from the attribute args
144    // and pass them straight through to `#[tokio::main(...)]`.
145    let tokio_attr = if args.is_empty() {
146        quote! { #[::tokio::main] }
147    } else {
148        quote! { #[::tokio::main(#args)] }
149    };
150
151    // Re-block the body so the tracing init runs before user code.
152    let body = input.block.clone();
153    input.block = syn::parse2(quote! {{
154        {
155            use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
156            // `try_init` so duplicate installers (e.g. tests already
157            // holding a subscriber) don't panic.
158            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    // Default to "./migrations" if invoked without args.
176    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    // Compile-time chain validation: read each migration's JSON,
216    // pull `name` and `prev` (file-stem-keyed for the chain check),
217    // and verify every `prev` points to another migration in the
218    // slice. Mismatches between the file stem and the embedded
219    // `name` field — and broken `prev` chains — fail at MACRO
220    // EXPANSION time so a misshapen migration set never compiles.
221    //
222    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
223    // migrations + a Rust proc-macro that reads them is the unique
224    // combo nothing else in the Django-shape Rust camp can match
225    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
226    // Rwf's are raw SQL — none have a static chain to validate).
227    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    // Validate that #[rustango(display = "…")] names a real field.
336    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    // Validate admin field-name lists against declared field names.
348    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!()` evaluates at the registration site,
459                // so a Model declared in `crate::blog::models` records
460                // `"<crate>::blog::models"` and `resolved_app_label()`
461                // can infer "blog" without an explicit attribute.
462                module_path: ::core::module_path!(),
463            }
464        }
465    })
466}
467
468/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
469/// matches `field_name` against the model's FK fields and, for a
470/// match, decodes the FK target via the parent's macro-generated
471/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
472/// `ForeignKey::Loaded` on `self`.
473///
474/// Always emitted (with empty arms for FK-less models, which
475/// return `Ok(false)` for any field name) so the `T: LoadRelated`
476/// trait bound on `fetch_on` is universally satisfied — users
477/// never have to think about implementing it.
478fn 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        // FK field's Rust ident matches its SQL column name in v0.8
486        // (no `column = "..."` rename ships on FK fields).
487        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
518/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
519/// matches `field_name` against the model's FK fields and returns
520/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
521/// group children by parent PK.
522///
523/// Always emitted (with `_ => None` for FK-less models) so the
524/// trait bound on `fetch_with_prefetch` is universally satisfied.
525fn 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
549/// For every `ForeignKey<Parent>` field on `Child`, emit
550/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
551/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
552/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
553/// — the canonical reverse-FK fetch. One round trip, no N+1.
554fn 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    // Snake-case the child struct name to derive the method suffix —
562    // `Post` → `post_set`, `BlogComment` → `blog_comment_set`. Avoids
563    // English-plural edge cases (Django's `<child>_set` convention).
564    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                ///
579                /// # Errors
580                /// Returns [`::rustango::sql::ExecError`] for SQL-writing
581                /// or driver failures.
582                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    /// The struct field ident, used both for the inherent const name on
609    /// the model and for the inner column type's name.
610    ident: syn::Ident,
611    /// The struct's field type, used as `Column::Value`.
612    value_ty: Type,
613    /// Rust-side field name (e.g. `"id"`).
614    name: String,
615    /// SQL-side column name (e.g. `"user_id"`).
616    column: String,
617    /// `::rustango::core::FieldType::I64` etc.
618    field_type_tokens: TokenStream2,
619}
620
621struct CollectedFields {
622    field_schemas: Vec<TokenStream2>,
623    from_row_inits: Vec<TokenStream2>,
624    /// Aliased counterparts of `from_row_inits` — read columns via
625    /// `format!("{prefix}__{col}")` aliases so a Model can be
626    /// decoded from a JOINed row's projected target columns.
627    from_aliased_row_inits: Vec<TokenStream2>,
628    /// Static column-name list — used by the simple insert path
629    /// (no `Auto<T>` fields). Aligned with `insert_values`.
630    insert_columns: Vec<TokenStream2>,
631    /// Static `Into<SqlValue>` expressions, one per field. Aligned
632    /// with `insert_columns`. Used by the simple insert path only.
633    insert_values: Vec<TokenStream2>,
634    /// Per-field push expressions for the dynamic (Auto-aware)
635    /// insert path. Each statement either unconditionally pushes
636    /// `(column, value)` or, for an `Auto<T>` field, conditionally
637    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
638    insert_pushes: Vec<TokenStream2>,
639    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
640    /// when `has_auto == false`.
641    returning_cols: Vec<TokenStream2>,
642    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
643    /// field. Run after `insert_returning` to populate the model.
644    auto_assigns: Vec<TokenStream2>,
645    /// `(ident, column_literal)` pairs for every Auto field. Used by
646    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
647    /// instead of `self`.
648    auto_field_idents: Vec<(syn::Ident, String)>,
649    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
650    /// by the all-Auto-Unset bulk path (Auto cols dropped from
651    /// `columns`).
652    bulk_pushes_no_auto: Vec<TokenStream2>,
653    /// Bulk-insert per-row pushes for **all fields including Auto**.
654    /// Used by the all-Auto-Set bulk path (Auto col included with the
655    /// caller-supplied value).
656    bulk_pushes_all: Vec<TokenStream2>,
657    /// Column-name literals for non-Auto fields only (paired with
658    /// `bulk_pushes_no_auto`).
659    bulk_columns_no_auto: Vec<TokenStream2>,
660    /// Column-name literals for every field including Auto (paired
661    /// with `bulk_pushes_all`).
662    bulk_columns_all: Vec<TokenStream2>,
663    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
664    /// + the loop that asserts every row matches. One pair per Auto
665    /// field. Empty when `has_auto == false`.
666    bulk_auto_uniformity: Vec<TokenStream2>,
667    /// Identifier of the first Auto field, used as the witness for
668    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
669    first_auto_ident: Option<syn::Ident>,
670    /// `true` if any field on the struct is `Auto<T>`.
671    has_auto: bool,
672    /// `true` when the primary-key field's Rust type is `Auto<T>`.
673    /// Gates `save()` codegen — only Auto PKs let us infer
674    /// insert-vs-update from the in-memory value.
675    pk_is_auto: bool,
676    /// `Assignment` constructors for every non-PK column. Drives the
677    /// UPDATE branch of `save()`.
678    update_assignments: Vec<TokenStream2>,
679    primary_key: Option<(syn::Ident, String)>,
680    column_entries: Vec<ColumnEntry>,
681    /// Rust-side field names, in declaration order. Used to validate
682    /// container attributes like `display = "…"`.
683    field_names: Vec<String>,
684    /// FK fields on this child model. Drives the reverse-relation
685    /// helper emit — for each FK, the macro adds an inherent
686    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
687    /// method on the parent type.
688    fk_relations: Vec<FkRelation>,
689    /// SQL column name of the `#[rustango(soft_delete)]` field, if
690    /// the model has one. Drives emission of the `soft_delete_on` /
691    /// `restore_on` inherent methods. At most one such column per
692    /// model is allowed; collect_fields rejects duplicates.
693    soft_delete_column: Option<String>,
694}
695
696#[derive(Clone)]
697struct FkRelation {
698    /// Inner type of `ForeignKey<T>` — the parent model. The reverse
699    /// helper is emitted as `impl <ParentType> { … }`.
700    parent_type: Type,
701    /// SQL column name on the child table for this FK (e.g. `"author"`).
702    /// Used in the generated `WHERE <fk_column> = $1` clause.
703    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            // Bulk: Auto fields appear only in the all-Set path,
783            // never in the Unset path (we drop them from `columns`).
784            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            // Uniformity check: every row's Auto state must match the
791            // first row's. Mixed Set/Unset within one bulk_insert is
792            // rejected here so the column list stays consistent.
793            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            // Bulk: non-Auto fields appear in BOTH paths.
813            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            // Immutable post-insert: skip from UPDATE entirely.
836        } else if info.auto_now {
837            // `auto_now` columns: bind `chrono::Utc::now()` on every
838            // UPDATE so the column is always overridden with the
839            // wall-clock at write time, regardless of what value the
840            // user left in the struct field.
841            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
911/// Emit the `admin: Option<&'static AdminConfig>` field for the model
912/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
913/// otherwise a static reference to a populated `AdminConfig`.
914fn 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    // Audit-emit fragments threaded into write paths. Non-empty only
1003    // when the model carries `#[rustango(audit(...))]`. They reborrow
1004    // `_executor` (a `&mut PgConnection` for audited models — the
1005    // macro switches the signature below) so the data write and the
1006    // audit INSERT both run on the same caller-supplied connection.
1007    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    // For audited models the `_on` methods take `&mut PgConnection`, so
1031    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
1032    // must acquire a connection first. Non-audited models keep the
1033    // direct delegation since `&PgPool` IS an Executor.
1034    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    // Build the (column, JSON value) pair list used by every
1068    // snapshot-style audit emission. Reused across delete_on,
1069    // soft_delete_on, restore_on, and (later) bulk paths. Empty
1070    // when the model isn't audited.
1071    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    // Update emission captures both BEFORE and AFTER state — runs an
1124    // extra SELECT against `_executor` BEFORE the UPDATE, captures
1125    // each tracked field's prior value, then after the UPDATE diffs
1126    // against the in-memory `&self`. `diff_changes` drops unchanged
1127    // columns so the JSON only contains the actual delta.
1128    //
1129    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
1130    // and binds `_audit_before_pairs`; `audit_update_post` runs
1131    // after the UPDATE and emits the PendingEntry.
1132    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    // Bulk-insert audit: capture every row's tracked fields after the
1231    // RETURNING populates each PK, then push one batched INSERT INTO
1232    // audit_log via `emit_many`. One round-trip regardless of N rows.
1233    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            /// Insert this row if its `Auto<T>` primary key is
1286            /// `Unset`, otherwise update the existing row matching the
1287            /// PK. Mirrors Django's `save()` — caller doesn't need to
1288            /// pick `insert` vs the bulk-update path manually.
1289            ///
1290            /// On the insert branch, populates the PK from `RETURNING`
1291            /// (same behavior as `insert`). On the update branch,
1292            /// writes every non-PK column back; if no row matches the
1293            /// PK, returns `Ok(())` silently.
1294            ///
1295            /// Only generated when the primary key is declared as
1296            /// `Auto<T>`. Models with a manually-managed PK must use
1297            /// `insert` or the QuerySet update builder.
1298            ///
1299            /// # Errors
1300            /// Returns [`::rustango::sql::ExecError`] for SQL-writing
1301            /// or driver failures.
1302            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            /// Like [`Self::save`] but accepts any sqlx executor —
1310            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
1311            /// escape hatch for tenant-scoped writes: schema-mode
1312            /// tenants share the registry pool but rely on a per-
1313            /// checkout `SET search_path`, so passing `&PgPool` would
1314            /// silently hit the wrong schema. Acquire a connection
1315            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
1316            ///
1317            /// # Errors
1318            /// As [`Self::save`].
1319            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            /// Per-call override for the audit source. Runs
1351            /// [`Self::save_on`] inside an [`::rustango::audit::with_source`]
1352            /// scope so the resulting audit entry records `source`
1353            /// instead of the task-local default. Useful for seed
1354            /// scripts and one-off CLI tools that don't sit inside an
1355            /// admin handler. The override applies only to this call;
1356            /// no global state changes.
1357            ///
1358            /// # Errors
1359            /// As [`Self::save_on`].
1360            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        // Optional `soft_delete_on` / `restore_on` companions when the
1377        // model has a `#[rustango(soft_delete)]` column. They land
1378        // alongside the regular `delete_on` so callers have both
1379        // options — a hard delete (audit-tracked as a real DELETE) and
1380        // a logical delete (audit-tracked as an UPDATE setting the
1381        // deleted_at column to NOW()).
1382        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
1383            let col_lit = col;
1384            quote! {
1385                /// Soft-delete this row by setting its
1386                /// `#[rustango(soft_delete)]` column to `NOW()`.
1387                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
1388                /// the row stays in the table; query helpers can
1389                /// filter it out by checking the column for `IS NOT
1390                /// NULL`.
1391                ///
1392                /// # Errors
1393                /// As [`Self::delete`].
1394                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                /// Inverse of [`Self::soft_delete_on`] — clears the
1429                /// soft-delete column back to NULL so the row is
1430                /// considered live again.
1431                ///
1432                /// # Errors
1433                /// As [`Self::delete`].
1434                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            /// Delete the row identified by this instance's primary key.
1471            ///
1472            /// Returns the number of rows affected (0 or 1).
1473            ///
1474            /// # Errors
1475            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1476            /// driver failures.
1477            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            /// Like [`Self::delete`] but accepts any sqlx executor —
1485            /// for tenant-scoped deletes against an explicitly-acquired
1486            /// connection. See [`Self::save_on`] for the rationale.
1487            ///
1488            /// # Errors
1489            /// As [`Self::delete`].
1490            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            /// Per-call audit-source override for [`Self::delete_on`].
1517            /// See [`Self::save_on_with`] for shape rationale.
1518            ///
1519            /// # Errors
1520            /// As [`Self::delete_on`].
1521            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            /// Insert this row into its table. Skips columns whose
1540            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
1541            /// sequence fills them in, then reads each `Auto` column
1542            /// back via `RETURNING` and stores it on `self`.
1543            ///
1544            /// # Errors
1545            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1546            /// driver failures.
1547            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            /// Like [`Self::insert`] but accepts any sqlx executor.
1555            /// See [`Self::save_on`] for tenancy-scoped rationale.
1556            ///
1557            /// # Errors
1558            /// As [`Self::insert`].
1559            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            /// Per-call audit-source override for [`Self::insert_on`].
1586            /// See [`Self::save_on_with`] for shape rationale.
1587            ///
1588            /// # Errors
1589            /// As [`Self::insert_on`].
1590            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            /// Insert this row into its table.
1605            ///
1606            /// # Errors
1607            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1608            /// driver failures.
1609            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            /// Like [`Self::insert`] but accepts any sqlx executor.
1617            /// See [`Self::save_on`] for tenancy-scoped rationale.
1618            ///
1619            /// # Errors
1620            /// As [`Self::insert`].
1621            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            /// Bulk-insert `rows` in a single round-trip. Every row's
1653            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
1654            /// (sequence fills them in) or uniformly `Auto::Set(_)`
1655            /// (caller-supplied values). Mixed Set/Unset is rejected
1656            /// — call `insert` per row for that case.
1657            ///
1658            /// Empty slice is a no-op. Each row's `Auto` fields are
1659            /// populated from the `RETURNING` clause in input order
1660            /// before this returns.
1661            ///
1662            /// # Errors
1663            /// Returns [`::rustango::sql::ExecError`] for validation,
1664            /// SQL-writing, mixed-Auto rejection, or driver failures.
1665            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            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
1673            /// See [`Self::save_on`] for tenancy-scoped rationale.
1674            ///
1675            /// # Errors
1676            /// As [`Self::bulk_insert`].
1677            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            /// Bulk-insert `rows` in a single round-trip. Every row's
1745            /// fields are written verbatim — there are no `Auto<T>`
1746            /// fields on this model.
1747            ///
1748            /// Empty slice is a no-op.
1749            ///
1750            /// # Errors
1751            /// Returns [`::rustango::sql::ExecError`] for validation,
1752            /// SQL-writing, or driver failures.
1753            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            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
1761            /// See [`Self::save_on`] for tenancy-scoped rationale.
1762            ///
1763            /// # Errors
1764            /// As [`Self::bulk_insert`].
1765            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            /// Hidden runtime accessor for the primary-key value as a
1799            /// [`SqlValue`]. Used by reverse-relation helpers
1800            /// (`<parent>::<child>_set`) emitted from sibling models'
1801            /// FK fields. Not part of the public API.
1802            #[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        /// Decode a row's aliased target columns (produced by
1828        /// `select_related`'s LEFT JOIN) into a fresh instance of
1829        /// this model. Reads each column via
1830        /// `format!("{prefix}__{col}")`, matching the alias the
1831        /// SELECT writer emitted. Slice 9.0d.
1832        #[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            /// Start a new `QuerySet` over this model.
1849            #[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
1877/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
1878/// `auto_assigns` but reading from `_returning_row` and writing to
1879/// `_row_mut` instead of `self`.
1880fn 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
1893/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
1894fn 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
1906/// Emit a hidden per-model module carrying one zero-sized type per field,
1907/// each with a `Column` impl pointing back at the model.
1908fn 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            // Re-import the parent scope so field types referencing
1937            // sibling models (e.g. `ForeignKey<Author>`) resolve
1938            // inside this submodule. Without this we'd hit
1939            // `proc_macro_derive_resolution_fallback` warnings.
1940            #[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    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
1978    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
1979    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
1980    /// at runtime — this attribute is the override for cases where
1981    /// the inference is wrong (e.g. a model that conceptually belongs
1982    /// to one app but is physically in another module).
1983    app: Option<String>,
1984    /// Django ModelAdmin-shape per-model knobs from
1985    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
1986    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
1987    /// admin code falls back to `AdminConfig::DEFAULT`.
1988    admin: Option<AdminAttrs>,
1989    /// Per-model audit configuration from `#[rustango(audit(...))]`.
1990    /// `None` when the model isn't audited — write paths emit no
1991    /// audit entries. When present, single-row writes capture
1992    /// before/after for the listed fields and bulk writes batch
1993    /// snapshots into one INSERT into `rustango_audit_log`.
1994    audit: Option<AuditAttrs>,
1995}
1996
1997/// Parsed shape of `#[rustango(audit(track = "name, body", source =
1998/// "user"))]`. `track` is a comma-separated list of field names whose
1999/// before/after values land in the JSONB `changes` column. `source`
2000/// is informational only — it pins a default source when the model
2001/// is written outside any `audit::with_source(...)` scope (rare).
2002#[derive(Default)]
2003struct AuditAttrs {
2004    /// Field names to capture in the `changes` JSONB. Validated
2005    /// against declared scalar fields at compile time. Empty means
2006    /// "track every scalar field" — Django's audit-everything default.
2007    track: Option<(Vec<String>, proc_macro2::Span)>,
2008}
2009
2010/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
2011/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
2012/// comma-separated strings; we validate each ident against the model's
2013/// declared fields at compile time.
2014#[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    /// Bulk action names. No field-validation against model fields —
2023    /// these are action handlers, not column references.
2024    actions: Option<(Vec<String>, proc_macro2::Span)>,
2025    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
2026    /// sections, comma-separated fields per section, optional
2027    /// `Title:` prefix. Empty title omits the `<legend>`.
2028    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
2143/// Split a comma-separated field-name list (e.g. `"name, office"`) into
2144/// owned field names, trimming whitespace and skipping empty entries.
2145/// Field-name validation against the model is done by the caller.
2146fn 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
2154/// Parse the fieldsets DSL: pipe-separated sections, optional
2155/// `"Title:"` prefix on each, comma-separated field names after.
2156/// Examples:
2157/// * `"name, office"` → one untitled section with two fields
2158/// * `"Identity: name, office | Metadata: created_at"` → two titled
2159///   sections
2160///
2161/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
2162fn 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            // Split off an optional `Title:` prefix (first colon).
2168            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
2180/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
2181/// Returns `(field_name, desc)` pairs in the same order as the input.
2182fn 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    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
2204    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
2205    /// "gen_random_uuid()"`. The Rust field type must be
2206    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
2207    /// INSERTs so the DB DEFAULT fires.
2208    auto_uuid: bool,
2209    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
2210    /// Server-set on insert, immutable from app code afterwards.
2211    /// Implies `auto + default = "now()"`. Field type must be
2212    /// `DateTime<Utc>`.
2213    auto_now_add: bool,
2214    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
2215    /// every insert AND every update. Implies `auto + default =
2216    /// "now()"`; the macro additionally rewrites `update_on` /
2217    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
2218    /// field value.
2219    auto_now: bool,
2220    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
2221    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
2222    /// `soft_delete_on(executor)` and `restore_on(executor)`
2223    /// methods on the model.
2224    soft_delete: bool,
2225    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
2226    /// the column in the generated DDL.
2227    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                // Implied: PK + auto + DEFAULT gen_random_uuid().
2297                // Each is also explicitly settable; the explicit
2298                // value wins if conflicting.
2299                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
2333/// Parse a signed integer literal, accepting optional leading `-`.
2334fn 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    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
2369    /// skip this column when `Auto::Unset` and emit it under
2370    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
2371    auto: bool,
2372    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
2373    /// the `Column::Value` associated type for typed-column tokens.
2374    value_ty: &'a Type,
2375    /// `FieldType` variant tokens (`::rustango::core::FieldType::I64`).
2376    field_type_tokens: TokenStream2,
2377    schema: TokenStream2,
2378    from_row_init: TokenStream2,
2379    /// Variant of [`Self::from_row_init`] that reads the column via
2380    /// `format!("{prefix}__{col}")` so a model can be decoded out of
2381    /// the aliased columns of a JOINed row. Drives slice 9.0d's
2382    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
2383    /// helper that `select_related` calls when stitching loaded FKs.
2384    from_aliased_row_init: TokenStream2,
2385    /// Inner type from a `ForeignKey<T>` field, if any. The reverse-
2386    /// relation helper emit (`Author::<child>_set`) needs to know `T`
2387    /// to point the generated method at the right child model.
2388    fk_inner: Option<Type>,
2389    /// `true` when this column was marked `#[rustango(auto_now)]` —
2390    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
2391    /// column instead of the user-supplied value, so `updated_at`
2392    /// always reflects the latest write without the caller having
2393    /// to remember to set it.
2394    auto_now: bool,
2395    /// `true` when this column was marked `#[rustango(auto_now_add)]`
2396    /// — the column is server-set on INSERT (DB DEFAULT) and
2397    /// **immutable** afterwards. `update_on` / `save_on` skip the
2398    /// column entirely so a stale `created_at` value in memory never
2399    /// rewrites the persisted timestamp.
2400    auto_now_add: bool,
2401    /// `true` when this column was marked `#[rustango(soft_delete)]`.
2402    /// Triggers emission of `soft_delete_on(executor)` and
2403    /// `restore_on(executor)` on the model's inherent impl. There is
2404    /// at most one such column per model — emission asserts this.
2405    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    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
2426    // INSERT path: the user must wrap the field in `Auto<T>`, which
2427    // marks the column as DB-default-supplied. The mixin attrs then
2428    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
2429    // for `auto_now`, force the value on UPDATE too.
2430    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/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
2650/// about kinds without depending on `rustango-core` (which would require a
2651/// proc-macro/normal split it doesn't have today).
2652#[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/// Result of walking a field's Rust type. `kind` is the underlying
2688/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
2689/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
2690/// `Some(<T>)` when the field was `ForeignKey<T>` (or
2691/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
2692#[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        // `ForeignKey<T>` is stored as BIGINT — same column shape as
2771        // the v0.1 `i64` + `#[rustango(fk = …)]` form. The macro does
2772        // not recurse into `T` because `T` is a Model struct, not a
2773        // primitive — its identity is opaque to schema detection.
2774        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// ============================================================
2846//  #[derive(Form)]  —  slice 8.4B
2847// ============================================================
2848
2849/// Per-field `#[form(...)]` attributes recognised by the derive.
2850#[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/// Detected shape of a form field's Rust type.
2859#[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            // String + Bool don't go through `str::parse`; the codegen
2877            // handles them inline.
2878            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    // Pull the raw &str from the payload. Uses variable name `data` to
3022    // match the new `Form::parse(data: &HashMap<…>)` signature.
3023    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}