Skip to main content

rws_macros/
lib.rs

1//! Procedural macro attributes for [rust-web-server](https://crates.io/crates/rust-web-server).
2//!
3//! Import via the main crate with `features = ["macros"]`:
4//!
5//! ```toml
6//! [dependencies]
7//! rust-web-server = { version = "18", features = ["macros"] }
8//! ```
9//!
10//! # Attributes
11//!
12//! | Attribute | Equivalent |
13//! |-----------|------------|
14//! | `#[route(GET, "/path")]` | generic; any method |
15//! | `#[get("/path")]` | shorthand for GET |
16//! | `#[post("/path")]` | shorthand for POST |
17//! | `#[put("/path")]` | shorthand for PUT |
18//! | `#[patch("/path")]` | shorthand for PATCH |
19//! | `#[delete("/path")]` | shorthand for DELETE |
20//!
21//! On a classic handler — `fn(&Request, &PathParams, &ConnectionInfo[, &S])`
22//! — the attributes add a `Route: METHOD /path` doc comment and leave the
23//! function unchanged; register it with `routes!` or `.get()`, `.post()`, etc.
24//!
25//! On any other function (18.28.0) the arguments are extractors
26//! (`Path<T>`, `Query<T>`, `Json<T>`, `Form<T>`, `State<S>`, `&S`, `Body`,
27//! `BodyText`, `RequestHeaders`, `&Request`, `&ConnectionInfo`, any
28//! `FromRequest` type) and the return type anything `IntoResponse`: the
29//! function becomes a unit struct implementing
30//! `rust_web_server::handler::{Handler, AsyncHandler, RouteSpec}`, registered
31//! with `.route(handler)`, and the function itself stays callable as
32//! `handler::call(…)`. The route's parameters are checked against the
33//! `Path` argument at compile time. See `rust_web_server::handler`.
34//!
35//! ```ignore
36//! #[get("/posts/{id}")]
37//! fn show_post(Path(id): Path<u64>, State(db): State<Db>) -> Result<Json<Post>, AppError> { … }
38//!
39//! let app = App::with_state(db).route(show_post);
40//! ```
41//!
42//! ## `#[derive(FromPath)]`
43//!
44//! A struct with one `FromStr` field per route parameter, named after it,
45//! for `Path<T>`.
46//!
47//! ## `#[derive(Config)]`
48//!
49//! Generates `fn load() -> Result<Self, String>` that reads environment variables
50//! and parses them into the annotated field types. See `rust_web_server::config_binding`.
51//!
52//! ## `#[derive(Factory)]` (17.30.0)
53//!
54//! Beside `#[derive(Model)]`: `T::factory()` for tests, building instances
55//! with default field values. See `rust_web_server::test_client::factory`.
56
57use proc_macro::TokenStream;
58use quote::quote;
59use syn::{
60    parse::{Parse, ParseStream},
61    parse_macro_input, Data, DeriveInput, Fields, Ident, ItemFn, Lit, LitInt, LitStr, Token,
62};
63
64mod handler;
65
66// ── Shared helper ─────────────────────────────────────────────────────────────
67
68fn annotate(method: &str, path: LitStr, func: ItemFn) -> TokenStream {
69    handler::expand(method, path, func).into()
70}
71
72// ── #[route(METHOD, "/path")] ─────────────────────────────────────────────────
73
74struct RouteArgs {
75    method: Ident,
76    path: LitStr,
77}
78
79impl Parse for RouteArgs {
80    fn parse(input: ParseStream) -> syn::Result<Self> {
81        let method: Ident = input.parse()?;
82        input.parse::<Token![,]>()?;
83        let path: LitStr = input.parse()?;
84        Ok(RouteArgs { method, path })
85    }
86}
87
88/// Annotate a handler function with its HTTP method and path.
89///
90/// On a classic handler (`&Request, &PathParams, &ConnectionInfo[, state]`)
91/// it adds a `Route: METHOD /path` doc comment and otherwise leaves the
92/// function unchanged; register it with `routes!` or `.get()`. On a typed
93/// handler (extractor arguments) it generates the adapter — see the crate
94/// docs and `rust_web_server::handler`.
95///
96/// # Example
97///
98/// ```ignore
99/// use rust_web_server::route;
100/// use rust_web_server::request::Request;
101/// use rust_web_server::router::PathParams;
102/// use rust_web_server::server::ConnectionInfo;
103/// use rust_web_server::response::Response;
104/// use std::sync::Arc;
105///
106/// struct Db;
107///
108/// #[route(GET, "/users/:id")]
109/// fn get_user(
110///     req: &Request,
111///     params: &PathParams,
112///     conn: &ConnectionInfo,
113///     state: &Arc<Db>,
114/// ) -> Response {
115///     let id = params.get("id").unwrap_or("0");
116///     Response::new()
117/// }
118/// ```
119#[proc_macro_attribute]
120pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
121    let RouteArgs { method, path } = parse_macro_input!(args as RouteArgs);
122    let func = parse_macro_input!(input as ItemFn);
123    annotate(&method.to_string(), path, func)
124}
125
126// ── #[get("/path")], #[post("/path")], … ─────────────────────────────────────
127
128/// Shorthand for `#[route(GET, "/path")]`.
129///
130/// # Example
131///
132/// ```ignore
133/// use rust_web_server::get;
134/// # use rust_web_server::request::Request;
135/// # use rust_web_server::router::PathParams;
136/// # use rust_web_server::server::ConnectionInfo;
137/// # use rust_web_server::response::Response;
138///
139/// #[get("/healthz")]
140/// fn health(_: &Request, _: &PathParams, _: &ConnectionInfo) -> Response {
141///     Response::new()
142/// }
143/// ```
144#[proc_macro_attribute]
145pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
146    let path = parse_macro_input!(args as LitStr);
147    let func = parse_macro_input!(input as ItemFn);
148    annotate("GET", path, func)
149}
150
151/// Shorthand for `#[route(POST, "/path")]`.
152#[proc_macro_attribute]
153pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
154    let path = parse_macro_input!(args as LitStr);
155    let func = parse_macro_input!(input as ItemFn);
156    annotate("POST", path, func)
157}
158
159/// Shorthand for `#[route(PUT, "/path")]`.
160#[proc_macro_attribute]
161pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
162    let path = parse_macro_input!(args as LitStr);
163    let func = parse_macro_input!(input as ItemFn);
164    annotate("PUT", path, func)
165}
166
167/// Shorthand for `#[route(PATCH, "/path")]`.
168#[proc_macro_attribute]
169pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
170    let path = parse_macro_input!(args as LitStr);
171    let func = parse_macro_input!(input as ItemFn);
172    annotate("PATCH", path, func)
173}
174
175/// Shorthand for `#[route(DELETE, "/path")]`.
176#[proc_macro_attribute]
177pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
178    let path = parse_macro_input!(args as LitStr);
179    let func = parse_macro_input!(input as ItemFn);
180    annotate("DELETE", path, func)
181}
182
183// ── #[derive(FromPath)] ───────────────────────────────────────────────────────
184
185/// Derive `rust_web_server::handler::FromPath` for a struct with named
186/// fields: one route parameter per field, by name, each parsed with
187/// `FromStr`. A typed handler's route must have exactly these parameters
188/// (checked at compile time).
189///
190/// ```ignore
191/// #[derive(rust_web_server::FromPath)]
192/// struct PostKey { id: u64, slug: String }
193///
194/// #[get("/posts/{id}/{slug}")]
195/// fn post(Path(key): Path<PostKey>) -> String { format!("{} {}", key.id, key.slug) }
196/// ```
197#[proc_macro_derive(FromPath)]
198pub fn derive_from_path(input: TokenStream) -> TokenStream {
199    let ast = parse_macro_input!(input as DeriveInput);
200    handler::derive_from_path(ast)
201        .unwrap_or_else(|e| e.to_compile_error())
202        .into()
203}
204
205// ── #[derive(FromRequest)] ────────────────────────────────────────────────────
206
207/// Derive `FromRequest` for a named-field struct.
208///
209/// Each field must implement `FromRequest`. Fields are extracted in declaration
210/// order; the first failure short-circuits and returns that error response.
211///
212/// # Example
213///
214/// ```ignore
215/// use rust_web_server::extract::{BodyText, Query};
216///
217/// #[derive(rust_web_server::FromRequest)]
218/// struct Payload {
219///     body: BodyText,
220///     params: Query,
221/// }
222/// ```
223#[proc_macro_derive(FromRequest)]
224pub fn derive_from_request(input: TokenStream) -> TokenStream {
225    let ast = parse_macro_input!(input as DeriveInput);
226    impl_from_request(ast)
227        .unwrap_or_else(|e| e.to_compile_error())
228        .into()
229}
230
231// ── #[derive(Validate)] ───────────────────────────────────────────────────────
232
233/// Derive `Validate` for a named-field struct.
234///
235/// Annotate fields with `#[validate(...)]` rules. All failures are collected
236/// before returning, so the caller sees every invalid field in one response.
237///
238/// # Supported validators
239///
240/// | Syntax | Checks |
241/// |--------|--------|
242/// | `length(min = N)` | `field.chars().count() >= N` |
243/// | `length(max = N)` | `field.chars().count() <= N` |
244/// | `length(min = N, max = N)` | both bounds |
245/// | `range(min = N)` | `field as f64 >= N` |
246/// | `range(max = N)` | `field as f64 <= N` |
247/// | `range(min = N, max = N)` | both bounds |
248/// | `email` | local part, `@`, domain with `.` |
249/// | `required` | `!field.is_empty()` |
250/// | `url` | starts with `http://` or `https://` |
251///
252/// # Example
253///
254/// ```ignore
255/// #[derive(rust_web_server::Validate)]
256/// struct CreateUser {
257///     #[validate(length(min = 1, max = 50))]
258///     name: String,
259///     #[validate(email)]
260///     email: String,
261///     #[validate(range(min = 0, max = 150))]
262///     age: u8,
263/// }
264/// ```
265#[proc_macro_derive(Validate, attributes(validate))]
266pub fn derive_validate(input: TokenStream) -> TokenStream {
267    let ast = parse_macro_input!(input as DeriveInput);
268    impl_validate(ast)
269        .unwrap_or_else(|e| e.to_compile_error())
270        .into()
271}
272
273fn impl_validate(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
274    let span = input.ident.span();
275    let name = &input.ident;
276
277    let fields = match &input.data {
278        Data::Struct(s) => match &s.fields {
279            Fields::Named(f) => &f.named,
280            _ => {
281                return Err(syn::Error::new(
282                    span,
283                    "#[derive(Validate)] only supports structs with named fields",
284                ))
285            }
286        },
287        _ => {
288            return Err(syn::Error::new(
289                span,
290                "#[derive(Validate)] can only be derived on structs",
291            ))
292        }
293    };
294
295    let mut all_checks: Vec<proc_macro2::TokenStream> = Vec::new();
296
297    for field in fields {
298        let field_ident = field.ident.as_ref().unwrap();
299        let field_name = field_ident.to_string();
300        for attr in &field.attrs {
301            if attr.path().is_ident("validate") {
302                let checks = generate_field_checks(attr, field_ident, &field_name)?;
303                all_checks.extend(checks);
304            }
305        }
306    }
307
308    Ok(quote! {
309        const _: () = {
310            use ::rust_web_server as _rws;
311            impl _rws::validate::Validate for #name {
312                fn validate(&self) -> ::core::result::Result<(), _rws::validate::ValidationErrors> {
313                    let mut __errors = _rws::validate::ValidationErrors::new();
314                    #(#all_checks)*
315                    if __errors.is_empty() {
316                        ::core::result::Result::Ok(())
317                    } else {
318                        ::core::result::Result::Err(__errors)
319                    }
320                }
321            }
322        };
323    })
324}
325
326fn generate_field_checks(
327    attr: &syn::Attribute,
328    field_ident: &syn::Ident,
329    field_name: &str,
330) -> syn::Result<Vec<proc_macro2::TokenStream>> {
331    let mut checks: Vec<proc_macro2::TokenStream> = Vec::new();
332
333    attr.parse_nested_meta(|meta| {
334        if meta.path.is_ident("email") {
335            let msg = format!("{field_name} must be a valid email address");
336            checks.push(quote! {
337                if !_rws::validate::is_email(&self.#field_ident) {
338                    __errors.add(#field_name, #msg);
339                }
340            });
341        } else if meta.path.is_ident("required") {
342            let msg = format!("{field_name} must not be empty");
343            checks.push(quote! {
344                if self.#field_ident.is_empty() {
345                    __errors.add(#field_name, #msg);
346                }
347            });
348        } else if meta.path.is_ident("url") {
349            let msg = format!("{field_name} must be a valid URL (http:// or https://)");
350            checks.push(quote! {
351                if !_rws::validate::is_url(&self.#field_ident) {
352                    __errors.add(#field_name, #msg);
353                }
354            });
355        } else if meta.path.is_ident("length") {
356            let mut min: Option<u64> = None;
357            let mut max: Option<u64> = None;
358            meta.parse_nested_meta(|inner| {
359                if inner.path.is_ident("min") {
360                    let lit: LitInt = inner.value()?.parse()?;
361                    min = Some(lit.base10_parse()?);
362                } else if inner.path.is_ident("max") {
363                    let lit: LitInt = inner.value()?.parse()?;
364                    max = Some(lit.base10_parse()?);
365                } else {
366                    return Err(inner.error("expected `min` or `max`"));
367                }
368                Ok(())
369            })?;
370
371            let mut len_checks: Vec<proc_macro2::TokenStream> = Vec::new();
372            if let Some(n) = min {
373                let msg = format!("{field_name} must be at least {n} character(s) long");
374                let n_lit = proc_macro2::Literal::usize_suffixed(n as usize);
375                len_checks.push(quote! {
376                    if __len < #n_lit { __errors.add(#field_name, #msg); }
377                });
378            }
379            if let Some(n) = max {
380                let msg = format!("{field_name} must be at most {n} character(s) long");
381                let n_lit = proc_macro2::Literal::usize_suffixed(n as usize);
382                len_checks.push(quote! {
383                    if __len > #n_lit { __errors.add(#field_name, #msg); }
384                });
385            }
386            checks.push(quote! {
387                {
388                    let __len = self.#field_ident.chars().count();
389                    #(#len_checks)*
390                }
391            });
392        } else if meta.path.is_ident("range") {
393            let mut min: Option<f64> = None;
394            let mut max: Option<f64> = None;
395            meta.parse_nested_meta(|inner| {
396                if inner.path.is_ident("min") {
397                    min = Some(lit_to_f64(&inner.value()?.parse::<Lit>()?)?);
398                } else if inner.path.is_ident("max") {
399                    max = Some(lit_to_f64(&inner.value()?.parse::<Lit>()?)?);
400                } else {
401                    return Err(inner.error("expected `min` or `max`"));
402                }
403                Ok(())
404            })?;
405
406            let mut rng_checks: Vec<proc_macro2::TokenStream> = Vec::new();
407            if let Some(n) = min {
408                let msg = format!("{field_name} must be at least {n}");
409                let n_lit = proc_macro2::Literal::f64_suffixed(n);
410                rng_checks.push(quote! {
411                    if __val < #n_lit { __errors.add(#field_name, #msg); }
412                });
413            }
414            if let Some(n) = max {
415                let msg = format!("{field_name} must be at most {n}");
416                let n_lit = proc_macro2::Literal::f64_suffixed(n);
417                rng_checks.push(quote! {
418                    if __val > #n_lit { __errors.add(#field_name, #msg); }
419                });
420            }
421            checks.push(quote! {
422                {
423                    let __val = self.#field_ident as f64;
424                    #(#rng_checks)*
425                }
426            });
427        } else {
428            return Err(meta.error(
429                "unknown validator; expected: email, required, url, length, range",
430            ));
431        }
432        Ok(())
433    })?;
434
435    Ok(checks)
436}
437
438fn lit_to_f64(lit: &Lit) -> syn::Result<f64> {
439    match lit {
440        Lit::Float(f) => Ok(f.base10_parse()?),
441        Lit::Int(i) => Ok(i.base10_parse::<i64>()? as f64),
442        _ => Err(syn::Error::new_spanned(lit, "expected a numeric literal")),
443    }
444}
445
446fn impl_from_request(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
447    let span = input.ident.span();
448    let name = &input.ident;
449
450    let fields = match &input.data {
451        Data::Struct(s) => match &s.fields {
452            Fields::Named(f) => &f.named,
453            _ => {
454                return Err(syn::Error::new(
455                    span,
456                    "#[derive(FromRequest)] only supports structs with named fields",
457                ))
458            }
459        },
460        _ => {
461            return Err(syn::Error::new(
462                span,
463                "#[derive(FromRequest)] can only be derived on structs",
464            ))
465        }
466    };
467
468    let extractions = fields.iter().map(|f| {
469        let ident = f.ident.as_ref().unwrap();
470        let ty = &f.ty;
471        quote! {
472            let #ident = <#ty as _rws::extract::FromRequest>::from_request(__req)?;
473        }
474    });
475
476    let field_names = fields.iter().map(|f| f.ident.as_ref().unwrap());
477
478    Ok(quote! {
479        const _: () = {
480            use ::rust_web_server as _rws;
481            impl _rws::extract::FromRequest for #name {
482                fn from_request(__req: &_rws::request::Request) -> ::core::result::Result<Self, _rws::response::Response> {
483                    #(#extractions)*
484                    ::core::result::Result::Ok(#name { #(#field_names),* })
485                }
486            }
487        };
488    })
489}
490
491// ── #[derive(Config)] ─────────────────────────────────────────────────────────
492
493/// Derive `load() -> Result<Self, String>` for a configuration struct.
494///
495/// Each field is bound to an environment variable. The env var name is derived
496/// from the field name (uppercased) plus an optional struct-level prefix.
497///
498/// # Struct-level attribute
499///
500/// ```text
501/// #[config(prefix = "APP_")]
502/// ```
503///
504/// When set, every field's env var key is `prefix + key`.
505///
506/// # Field-level attribute
507///
508/// ```text
509/// #[config(env = "PORT", default = "8080")]
510/// ```
511///
512/// | Option | Meaning |
513/// |--------|---------|
514/// | `env = "KEY"` | explicit env var name (prefix is still prepended) |
515/// | `default = "v"` | fallback when the env var is absent |
516///
517/// If `env` is omitted the field name is uppercased and used as the key.
518/// Wrapping the field type in `Option<T>` makes it optional (returns `None` when absent).
519///
520/// # Example
521///
522/// ```ignore
523/// #[derive(rust_web_server::Config)]
524/// #[config(prefix = "APP_")]
525/// struct AppConfig {
526///     #[config(env = "PORT", default = "8080")]
527///     port: u16,
528///     #[config(env = "DATABASE_URL")]
529///     database_url: String,
530///     #[config(env = "DEBUG")]
531///     debug: Option<bool>,
532/// }
533///
534/// let cfg = AppConfig::load().unwrap();
535/// ```
536#[proc_macro_derive(Config, attributes(config))]
537pub fn derive_config(input: TokenStream) -> TokenStream {
538    let ast = parse_macro_input!(input as DeriveInput);
539    impl_config(ast)
540        .unwrap_or_else(|e| e.to_compile_error())
541        .into()
542}
543
544fn impl_config(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
545    let span = input.ident.span();
546    let name = &input.ident;
547
548    // Parse optional struct-level prefix: #[config(prefix = "...")]
549    let prefix = parse_config_prefix(&input.attrs)?;
550
551    let fields = match &input.data {
552        Data::Struct(s) => match &s.fields {
553            Fields::Named(f) => &f.named,
554            _ => {
555                return Err(syn::Error::new(
556                    span,
557                    "#[derive(Config)] only supports structs with named fields",
558                ))
559            }
560        },
561        _ => {
562            return Err(syn::Error::new(
563                span,
564                "#[derive(Config)] can only be derived on structs",
565            ))
566        }
567    };
568
569    let mut field_loads: Vec<proc_macro2::TokenStream> = Vec::new();
570    let mut field_names: Vec<&syn::Ident> = Vec::new();
571
572    for field in fields {
573        let ident = field.ident.as_ref().unwrap();
574        field_names.push(ident);
575
576        // Parse field-level #[config(env = "...", default = "...")]
577        let (env_key, default) = parse_field_config(&field.attrs, ident, &prefix)?;
578
579        let is_option = is_option_type(&field.ty);
580
581        let load_expr = if is_option {
582            quote! {
583                _rws::config_binding::load_optional(&#env_key)?
584            }
585        } else if let Some(default_str) = default {
586            quote! {
587                _rws::config_binding::load_with_default(&#env_key, #default_str)?
588            }
589        } else {
590            quote! {
591                _rws::config_binding::load_required(&#env_key)?
592            }
593        };
594
595        field_loads.push(quote! {
596            let #ident = #load_expr;
597        });
598    }
599
600    Ok(quote! {
601        impl #name {
602            /// Load configuration from environment variables.
603            pub fn load() -> ::core::result::Result<Self, ::std::string::String> {
604                use ::rust_web_server as _rws;
605                #(#field_loads)*
606                ::core::result::Result::Ok(#name {
607                    #(#field_names),*
608                })
609            }
610        }
611    })
612}
613
614/// Return `(env_key_expr, Option<default_str>)` for a field.
615/// `env_key_expr` is a `proc_macro2::TokenStream` that evaluates to a `String`.
616fn parse_field_config(
617    attrs: &[syn::Attribute],
618    ident: &syn::Ident,
619    prefix: &str,
620) -> syn::Result<(proc_macro2::TokenStream, Option<LitStr>)> {
621    let mut env_name: Option<String> = None;
622    let mut default: Option<LitStr> = None;
623
624    for attr in attrs {
625        if !attr.path().is_ident("config") {
626            continue;
627        }
628        attr.parse_nested_meta(|meta| {
629            if meta.path.is_ident("env") {
630                let lit: LitStr = meta.value()?.parse()?;
631                env_name = Some(lit.value());
632            } else if meta.path.is_ident("default") {
633                default = Some(meta.value()?.parse()?);
634            } else {
635                return Err(meta.error("unknown config key; expected `env` or `default`"));
636            }
637            Ok(())
638        })?;
639    }
640
641    let key = format!(
642        "{}{}",
643        prefix,
644        env_name.unwrap_or_else(|| ident.to_string().to_uppercase())
645    );
646
647    Ok((quote! { #key }, default))
648}
649
650/// Parse `#[config(prefix = "...")]` from struct attributes, returning the prefix string.
651fn parse_config_prefix(attrs: &[syn::Attribute]) -> syn::Result<String> {
652    let mut prefix = String::new();
653    for attr in attrs {
654        if !attr.path().is_ident("config") {
655            continue;
656        }
657        attr.parse_nested_meta(|meta| {
658            if meta.path.is_ident("prefix") {
659                let lit: LitStr = meta.value()?.parse()?;
660                prefix = lit.value();
661            } else {
662                return Err(meta.error("unknown struct config key; expected `prefix`"));
663            }
664            Ok(())
665        })?;
666    }
667    Ok(prefix)
668}
669
670/// Return true if `ty` is `Option<_>`.
671fn is_option_type(ty: &syn::Type) -> bool {
672    if let syn::Type::Path(tp) = ty {
673        if let Some(seg) = tp.path.segments.last() {
674            return seg.ident == "Option";
675        }
676    }
677    false
678}
679
680// ── #[derive(Model)] ──────────────────────────────────────────────────────────
681
682/// Derive `rust_web_server::model::Model` for a struct.
683///
684/// # Struct attributes
685///
686/// | Attribute | Meaning |
687/// |---|---|
688/// | `#[table(name = "tbl")]` | Override table name (default: struct name lowercased) |
689///
690/// # Field attributes
691///
692/// | Attribute | Meaning |
693/// |---|---|
694/// | `#[primary_key]` | Mark as primary key |
695/// | `#[primary_key(auto_increment)]` | Auto-increment primary key |
696/// | `#[column(name = "col")]` | Override column name |
697/// | `#[column(unique)]` | Mark column unique (informational) |
698/// | `#[ignore]` | Exclude from DB mapping |
699///
700/// # Example
701///
702/// ```ignore
703/// #[derive(rust_web_server::Model, Debug, Clone)]
704/// #[table(name = "users")]
705/// pub struct User {
706///     #[primary_key(auto_increment)]
707///     pub id: i64,
708///     #[column(name = "first_name")]
709///     pub name: String,
710///     pub email: String,
711///     #[ignore]
712///     pub display_label: String,
713/// }
714/// ```
715#[proc_macro_derive(Model, attributes(table, column, primary_key))]
716pub fn derive_model(input: TokenStream) -> TokenStream {
717    let ast = parse_macro_input!(input as DeriveInput);
718    impl_model(ast)
719        .unwrap_or_else(|e| e.to_compile_error())
720        .into()
721}
722
723fn impl_model(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
724    let span = input.ident.span();
725    let struct_name = &input.ident;
726
727    // ── Determine table name ──────────────────────────────────────────────────
728    let table_name = parse_table_name(&input.attrs, struct_name)?;
729
730    let fields = match &input.data {
731        Data::Struct(s) => match &s.fields {
732            Fields::Named(f) => &f.named,
733            _ => {
734                return Err(syn::Error::new(
735                    span,
736                    "#[derive(Model)] only supports structs with named fields",
737                ))
738            }
739        },
740        _ => {
741            return Err(syn::Error::new(
742                span,
743                "#[derive(Model)] can only be derived on structs",
744            ))
745        }
746    };
747
748    // ── Parse field metadata ──────────────────────────────────────────────────
749    struct FieldMeta {
750        field_ident: syn::Ident,
751        col_name: String,
752        #[allow(dead_code)]
753        is_pk: bool,
754        is_auto_increment: bool,
755        is_ignored: bool,
756    }
757
758    let mut pk_field: Option<FieldMeta> = None;
759    let mut regular_fields: Vec<FieldMeta> = Vec::new();
760
761    for field in fields {
762        let ident = field.ident.as_ref().unwrap().clone();
763        let mut col_name = ident.to_string();
764        let mut is_pk = false;
765        let mut is_auto_increment = false;
766        let mut is_ignored = false;
767
768        for attr in &field.attrs {
769            if attr.path().is_ident("ignore") {
770                is_ignored = true;
771            } else if attr.path().is_ident("primary_key") {
772                is_pk = true;
773                // Check for (auto_increment)
774                let _ = attr.parse_nested_meta(|meta| {
775                    if meta.path.is_ident("auto_increment") {
776                        is_auto_increment = true;
777                    }
778                    Ok(())
779                });
780            } else if attr.path().is_ident("column") {
781                let _ = attr.parse_nested_meta(|meta| {
782                    if meta.path.is_ident("name") {
783                        let lit: LitStr = meta.value()?.parse()?;
784                        col_name = lit.value();
785                    }
786                    // unique / nullable are informational — ignore
787                    Ok(())
788                });
789            }
790        }
791
792        let meta = FieldMeta {
793            field_ident: ident,
794            col_name,
795            is_pk,
796            is_auto_increment,
797            is_ignored,
798        };
799
800        if is_pk {
801            pk_field = Some(meta);
802        } else {
803            regular_fields.push(meta);
804        }
805    }
806
807    let pk = pk_field.ok_or_else(|| {
808        syn::Error::new(span, "#[derive(Model)] requires exactly one #[primary_key] field")
809    })?;
810
811    // ── column_names (all mapped fields, including PK) ────────────────────────
812    let mut all_col_names: Vec<String> = Vec::new();
813    all_col_names.push(pk.col_name.clone());
814    for f in regular_fields.iter().filter(|f| !f.is_ignored) {
815        all_col_names.push(f.col_name.clone());
816    }
817
818    let pk_col_name = pk.col_name.clone();
819    let pk_field_ident = &pk.field_ident;
820    let auto_inc = pk.is_auto_increment;
821
822    // ── from_row body ─────────────────────────────────────────────────────────
823    let pk_from_row = {
824        let col = &pk.col_name;
825        let fident = &pk.field_ident;
826        quote! { #fident: row.get(#col)? }
827    };
828
829    let regular_from_row: Vec<proc_macro2::TokenStream> = fields
830        .iter()
831        .filter(|f| {
832            let ident_str = f.ident.as_ref().unwrap().to_string();
833            // Skip the PK field (handled separately) and ignored fields.
834            let is_pk_field = ident_str == pk_field_ident.to_string();
835            !is_pk_field
836        })
837        .map(|f| {
838            let fident = f.ident.as_ref().unwrap();
839            // Check if ignored
840            let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
841            if is_ignored {
842                quote! { #fident: ::core::default::Default::default() }
843            } else {
844                // Determine column name override
845                let mut col = fident.to_string();
846                for attr in &f.attrs {
847                    if attr.path().is_ident("column") {
848                        let _ = attr.parse_nested_meta(|meta| {
849                            if meta.path.is_ident("name") {
850                                if let Ok(lit) = meta.value()?.parse::<LitStr>() {
851                                    col = lit.value();
852                                }
853                            }
854                            Ok(())
855                        });
856                    }
857                }
858                quote! { #fident: row.get(#col)? }
859            }
860        })
861        .collect();
862
863    // ── to_values body ────────────────────────────────────────────────────────
864    let pk_to_values = {
865        let col = &pk.col_name;
866        quote! {
867            (#col, ::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident))
868        }
869    };
870
871    let regular_to_values: Vec<proc_macro2::TokenStream> = fields
872        .iter()
873        .filter(|f| {
874            let ident_str = f.ident.as_ref().unwrap().to_string();
875            let is_pk_field = ident_str == pk_field_ident.to_string();
876            let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
877            !is_pk_field && !is_ignored
878        })
879        .map(|f| {
880            let fident = f.ident.as_ref().unwrap();
881            let mut col = fident.to_string();
882            for attr in &f.attrs {
883                if attr.path().is_ident("column") {
884                    let _ = attr.parse_nested_meta(|meta| {
885                        if meta.path.is_ident("name") {
886                            if let Ok(lit) = meta.value()?.parse::<LitStr>() {
887                                col = lit.value();
888                            }
889                        }
890                        Ok(())
891                    });
892                }
893            }
894            quote! {
895                (#col, ::rust_web_server::model::ToColumn::to_column(&self.#fident))
896            }
897        })
898        .collect();
899
900    // ── Generate impl ─────────────────────────────────────────────────────────
901    Ok(quote! {
902        impl ::rust_web_server::model::Model for #struct_name {
903            fn table_name() -> &'static str {
904                #table_name
905            }
906
907            fn column_names() -> &'static [&'static str] {
908                &[#(#all_col_names),*]
909            }
910
911            fn primary_key_name() -> &'static str {
912                #pk_col_name
913            }
914
915            fn primary_key_value(&self) -> ::rust_web_server::model::Value {
916                ::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident)
917            }
918
919            fn primary_key_auto_increment() -> bool {
920                #auto_inc
921            }
922
923            fn from_row(row: &::rust_web_server::model::ModelRow) -> ::core::result::Result<Self, ::rust_web_server::model::DbError> {
924                ::core::result::Result::Ok(#struct_name {
925                    #pk_from_row,
926                    #(#regular_from_row),*
927                })
928            }
929
930            fn to_values(&self) -> ::std::vec::Vec<(&'static str, ::rust_web_server::model::Value)> {
931                vec![
932                    #pk_to_values,
933                    #(#regular_to_values),*
934                ]
935            }
936        }
937
938        impl #struct_name {
939            /// Create a `ModelRepository` tied to the given pool.
940            pub fn repository(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::ModelRepository<#struct_name, i64> {
941                ::rust_web_server::model::ModelRepository::new(pool)
942            }
943
944            /// Create a `QueryBuilder` tied to the given pool.
945            pub fn query(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::QueryBuilder<#struct_name> {
946                ::rust_web_server::model::QueryBuilder::new(pool)
947            }
948        }
949    })
950}
951
952fn parse_table_name(
953    attrs: &[syn::Attribute],
954    struct_name: &syn::Ident,
955) -> syn::Result<String> {
956    for attr in attrs {
957        if attr.path().is_ident("table") {
958            let mut name: Option<String> = None;
959            attr.parse_nested_meta(|meta| {
960                if meta.path.is_ident("name") {
961                    let lit: LitStr = meta.value()?.parse()?;
962                    name = Some(lit.value());
963                }
964                Ok(())
965            })?;
966            if let Some(n) = name {
967                return Ok(n);
968            }
969        }
970    }
971    // Default: lowercased struct name.
972    Ok(struct_name.to_string().to_lowercase())
973}
974
975// ── #[derive(Factory)] ────────────────────────────────────────────────────────
976
977/// Derive `rust_web_server::test_client::factory::Factory` for a
978/// `#[derive(Model)]` struct, and an inherent `T::factory()` returning a
979/// `ModelFactory<T>` for tests (17.30.0).
980///
981/// Each field gets its type's `FactoryValue` default (numbers `0`, `bool`
982/// `false`, `Option` `None`, a `String` the field's name plus a sequence
983/// number), or `Default::default()` for a type without one and for
984/// `#[ignore]` fields. The `#[primary_key]` is `0` when it's
985/// `auto_increment`, else the sequence number. It reads the same attributes
986/// as `#[derive(Model)]` and declares none of its own.
987///
988/// ```ignore
989/// #[derive(rust_web_server::Model, rust_web_server::Factory, Debug, Clone)]
990/// #[table(name = "users")]
991/// pub struct User {
992///     #[primary_key(auto_increment)]
993///     pub id: i64,
994///     pub name: String,
995/// }
996///
997/// let user = User::factory().with(|u| u.name = "Ann".into()).create(&pool).await?;
998/// ```
999#[proc_macro_derive(Factory)]
1000pub fn derive_factory(input: TokenStream) -> TokenStream {
1001    let ast = parse_macro_input!(input as DeriveInput);
1002    impl_factory(ast)
1003        .unwrap_or_else(|e| e.to_compile_error())
1004        .into()
1005}
1006
1007fn impl_factory(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1008    let span = input.ident.span();
1009    let struct_name = &input.ident;
1010    if !input.generics.params.is_empty() {
1011        return Err(syn::Error::new(span, "#[derive(Factory)] doesn't support generic structs"));
1012    }
1013    let fields = match &input.data {
1014        Data::Struct(s) => match &s.fields {
1015            Fields::Named(f) => &f.named,
1016            _ => return Err(syn::Error::new(span, "#[derive(Factory)] only supports structs with named fields")),
1017        },
1018        _ => return Err(syn::Error::new(span, "#[derive(Factory)] can only be derived on structs")),
1019    };
1020
1021    let mut has_pk = false;
1022    let mut inits: Vec<proc_macro2::TokenStream> = Vec::new();
1023    for field in fields {
1024        let ident = field.ident.as_ref().unwrap();
1025        let ty = &field.ty;
1026        let name = ident.to_string();
1027        let name = name.strip_prefix("r#").unwrap_or(&name).to_string();
1028        let mut is_pk = false;
1029        let mut auto_increment = false;
1030        let mut ignored = false;
1031        for attr in &field.attrs {
1032            if attr.path().is_ident("ignore") {
1033                ignored = true;
1034            } else if attr.path().is_ident("primary_key") {
1035                is_pk = true;
1036                let _ = attr.parse_nested_meta(|meta| {
1037                    if meta.path.is_ident("auto_increment") {
1038                        auto_increment = true;
1039                    }
1040                    Ok(())
1041                });
1042            }
1043        }
1044        has_pk |= is_pk;
1045        let probe = quote! { (&&__Probe::<#ty>(::core::marker::PhantomData)) };
1046        inits.push(if ignored {
1047            quote! { #ident: ::core::default::Default::default() }
1048        } else if is_pk && !auto_increment {
1049            quote! { #ident: #probe.make_key(#name, __seq) }
1050        } else {
1051            quote! { #ident: #probe.make(#name, __seq) }
1052        });
1053    }
1054    if !has_pk {
1055        return Err(syn::Error::new(span, "#[derive(Factory)] needs the #[primary_key] field of #[derive(Model)]"));
1056    }
1057
1058    Ok(quote! {
1059        impl ::rust_web_server::test_client::factory::Factory for #struct_name {
1060            fn factory_default(__seq: u64) -> Self {
1061                #[allow(unused_imports)]
1062                use ::rust_web_server::test_client::factory::__private::{
1063                    Probe as __Probe, ViaDefault as _, ViaValue as _,
1064                };
1065                #struct_name {
1066                    #(#inits),*
1067                }
1068            }
1069        }
1070
1071        impl #struct_name {
1072            /// A factory building this model with default values, for tests.
1073            pub fn factory() -> ::rust_web_server::test_client::factory::ModelFactory<#struct_name> {
1074                ::rust_web_server::test_client::factory::ModelFactory::new()
1075            }
1076        }
1077    })
1078}
1079
1080// ── #[derive(JsonSchema)] ─────────────────────────────────────────────────────
1081
1082/// Derive `rust_web_server::schema::JsonSchema` for a struct with named
1083/// fields or an enum of unit variants.
1084///
1085/// The schema follows the type's serde attributes, so it describes the JSON
1086/// `serde_json` reads and writes: container `rename_all` and `default`;
1087/// field `rename`, `default`, `skip`/`skip_serializing`/`skip_deserializing`
1088/// (left out) and `skip_serializing_if` (not required); variant `rename`.
1089/// Attributes that change the JSON's shape — `flatten`, `tag`, `content`,
1090/// `untagged`, `transparent`, `with`/`serialize_with`/`deserialize_with`,
1091/// separate serialize/deserialize renames — are a compile error rather than a
1092/// schema that doesn't match. `Option` fields are not required. Doc comments
1093/// become `description`s.
1094///
1095/// ```ignore
1096/// #[derive(serde::Serialize, serde::Deserialize, rust_web_server::JsonSchema)]
1097/// #[serde(rename_all = "camelCase")]
1098/// struct Item {
1099///     /// Stock keeping unit.
1100///     sku: String,
1101///     in_stock: u32,
1102///     note: Option<String>,
1103/// }
1104/// ```
1105#[proc_macro_derive(JsonSchema, attributes(serde))]
1106pub fn derive_json_schema(input: TokenStream) -> TokenStream {
1107    let ast = parse_macro_input!(input as DeriveInput);
1108    impl_json_schema(ast)
1109        .unwrap_or_else(|e| e.to_compile_error())
1110        .into()
1111}
1112
1113fn impl_json_schema(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1114    let name = &input.ident;
1115    let container = SerdeAttrs::parse(&input.attrs)?;
1116    let description = doc_comment(&input.attrs);
1117    let body = match &input.data {
1118        Data::Struct(s) => {
1119            let Fields::Named(fields) = &s.fields else {
1120                return Err(syn::Error::new_spanned(
1121                    name,
1122                    "#[derive(JsonSchema)] supports structs with named fields and enums of unit variants",
1123                ));
1124            };
1125            let mut properties = Vec::new();
1126            for field in &fields.named {
1127                let attrs = SerdeAttrs::parse(&field.attrs)?;
1128                if attrs.skip {
1129                    continue;
1130                }
1131                let ident = field.ident.as_ref().expect("named field");
1132                let json_name = attrs.rename.clone().unwrap_or_else(|| {
1133                    rename_with(ident.to_string().trim_start_matches("r#"), container.rename_all.as_deref(), false)
1134                });
1135                let ty = &field.ty;
1136                let field_doc = doc_comment(&field.attrs);
1137                let may_be_absent = attrs.default || container.default || attrs.skip_serializing_if;
1138                properties.push(quote! {
1139                    __schema.property(
1140                        #json_name,
1141                        &_rws::schema::nested::<#ty>(),
1142                        #field_doc,
1143                        !#may_be_absent && !<#ty as _rws::schema::JsonSchema>::is_optional(),
1144                    );
1145                });
1146            }
1147            quote! {
1148                let mut __schema = _rws::schema::ObjectSchema::new(#description);
1149                #(#properties)*
1150                __schema.finish()
1151            }
1152        }
1153        Data::Enum(e) => {
1154            let mut values = Vec::new();
1155            for variant in &e.variants {
1156                if !matches!(variant.fields, Fields::Unit) {
1157                    return Err(syn::Error::new_spanned(
1158                        variant,
1159                        "#[derive(JsonSchema)] supports only unit enum variants",
1160                    ));
1161                }
1162                let attrs = SerdeAttrs::parse(&variant.attrs)?;
1163                if attrs.skip {
1164                    continue;
1165                }
1166                values.push(attrs.rename.unwrap_or_else(|| {
1167                    rename_with(&variant.ident.to_string(), container.rename_all.as_deref(), true)
1168                }));
1169            }
1170            quote! { _rws::schema::string_enum(#description, &[#(#values),*]) }
1171        }
1172        Data::Union(_) => {
1173            return Err(syn::Error::new_spanned(name, "#[derive(JsonSchema)] does not support unions"));
1174        }
1175    };
1176
1177    let mut generics = input.generics.clone();
1178    for param in generics.type_params_mut() {
1179        param.bounds.push(syn::parse_quote!(::rust_web_server::schema::JsonSchema));
1180    }
1181    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1182    Ok(quote! {
1183        const _: () = {
1184            use ::rust_web_server as _rws;
1185            impl #impl_generics _rws::schema::JsonSchema for #name #ty_generics #where_clause {
1186                fn json_schema() -> ::std::string::String {
1187                    #body
1188                }
1189            }
1190        };
1191    })
1192}
1193
1194/// The `#[serde(...)]` attributes `#[derive(JsonSchema)]` understands.
1195#[derive(Default)]
1196struct SerdeAttrs {
1197    rename: Option<String>,
1198    rename_all: Option<String>,
1199    default: bool,
1200    skip: bool,
1201    skip_serializing_if: bool,
1202}
1203
1204impl SerdeAttrs {
1205    fn parse(attrs: &[syn::Attribute]) -> syn::Result<Self> {
1206        let mut out = SerdeAttrs::default();
1207        for attr in attrs.iter().filter(|a| a.path().is_ident("serde")) {
1208            attr.parse_nested_meta(|meta| {
1209                let key = meta.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
1210                match key.as_str() {
1211                    "rename" | "rename_all" => {
1212                        if !meta.input.peek(Token![=]) {
1213                            return Err(meta.error(format!(
1214                                "#[derive(JsonSchema)] needs one `{key}` for both directions (one schema describes input and output)"
1215                            )));
1216                        }
1217                        let value = meta.value()?.parse::<LitStr>()?.value();
1218                        if key == "rename" {
1219                            out.rename = Some(value);
1220                        } else {
1221                            if rename_with("a_b", Some(&value), false).is_empty() {
1222                                return Err(meta.error(format!("unknown rename_all rule \"{value}\"")));
1223                            }
1224                            out.rename_all = Some(value);
1225                        }
1226                    }
1227                    "default" => {
1228                        out.default = true;
1229                        if meta.input.peek(Token![=]) {
1230                            meta.value()?.parse::<LitStr>()?;
1231                        }
1232                    }
1233                    "skip" | "skip_serializing" | "skip_deserializing" => out.skip = true,
1234                    "skip_serializing_if" => {
1235                        out.skip_serializing_if = true;
1236                        meta.value()?.parse::<LitStr>()?;
1237                    }
1238                    "flatten" | "tag" | "content" | "untagged" | "transparent" | "with" | "serialize_with"
1239                    | "deserialize_with" | "remote" | "from" | "try_from" | "into" | "getter" => {
1240                        return Err(meta.error(format!(
1241                            "#[derive(JsonSchema)] does not support #[serde({key})]: the schema would not match the JSON"
1242                        )));
1243                    }
1244                    _ => {
1245                        // Attributes that don't change the JSON's shape
1246                        // (alias, deny_unknown_fields, bound, crate, borrow,
1247                        // expecting, ...): skip their value, if any.
1248                        if meta.input.peek(Token![=]) {
1249                            meta.value()?.parse::<proc_macro2::TokenStream>()?;
1250                        } else if meta.input.peek(syn::token::Paren) {
1251                            meta.parse_nested_meta(|nested| {
1252                                if nested.input.peek(Token![=]) {
1253                                    nested.value()?.parse::<proc_macro2::TokenTree>()?;
1254                                }
1255                                Ok(())
1256                            })?;
1257                        }
1258                    }
1259                }
1260                Ok(())
1261            })?;
1262        }
1263        Ok(out)
1264    }
1265}
1266
1267/// `name` renamed by serde's `rename_all` `rule` (`variant`: `name` is a
1268/// PascalCase enum variant rather than a snake_case field). Empty for an
1269/// unknown rule.
1270fn rename_with(name: &str, rule: Option<&str>, variant: bool) -> String {
1271    let Some(rule) = rule else { return name.to_string() };
1272    // Split into lower-case words.
1273    let words: Vec<String> = if variant {
1274        let mut words = Vec::new();
1275        let mut current = String::new();
1276        for c in name.chars() {
1277            if c.is_uppercase() && !current.is_empty() {
1278                words.push(std::mem::take(&mut current));
1279            }
1280            current.extend(c.to_lowercase());
1281        }
1282        if !current.is_empty() {
1283            words.push(current);
1284        }
1285        words
1286    } else {
1287        name.split('_').map(str::to_string).collect()
1288    };
1289    let capitalize = |w: &String| {
1290        let mut chars = w.chars();
1291        chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
1292    };
1293    match rule {
1294        "lowercase" => words.concat(),
1295        "UPPERCASE" => words.concat().to_uppercase(),
1296        "PascalCase" => words.iter().map(capitalize).collect(),
1297        "camelCase" => {
1298            let pascal: String = words.iter().map(capitalize).collect();
1299            let mut chars = pascal.chars();
1300            chars.next().map(|c| c.to_lowercase().chain(chars).collect()).unwrap_or_default()
1301        }
1302        "snake_case" => words.join("_"),
1303        "SCREAMING_SNAKE_CASE" => words.join("_").to_uppercase(),
1304        "kebab-case" => words.join("-"),
1305        "SCREAMING-KEBAB-CASE" => words.join("-").to_uppercase(),
1306        _ => String::new(),
1307    }
1308}
1309
1310/// The item's `///` doc comment, lines joined with spaces.
1311fn doc_comment(attrs: &[syn::Attribute]) -> String {
1312    let mut lines = Vec::new();
1313    for attr in attrs.iter().filter(|a| a.path().is_ident("doc")) {
1314        if let syn::Meta::NameValue(nv) = &attr.meta {
1315            if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value {
1316                let line = s.value().trim().to_string();
1317                if !line.is_empty() {
1318                    lines.push(line);
1319                }
1320            }
1321        }
1322    }
1323    lines.join(" ")
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328    use super::rename_with;
1329
1330    #[test]
1331    fn rename_all_rules_match_serde() {
1332        let field = |rule| rename_with("line_item_id", Some(rule), false);
1333        assert_eq!("lineitemid", field("lowercase"));
1334        assert_eq!("LINEITEMID", field("UPPERCASE"));
1335        assert_eq!("LineItemId", field("PascalCase"));
1336        assert_eq!("lineItemId", field("camelCase"));
1337        assert_eq!("line_item_id", field("snake_case"));
1338        assert_eq!("LINE_ITEM_ID", field("SCREAMING_SNAKE_CASE"));
1339        assert_eq!("line-item-id", field("kebab-case"));
1340        assert_eq!("LINE-ITEM-ID", field("SCREAMING-KEBAB-CASE"));
1341        let variant = |rule| rename_with("BackOrdered", Some(rule), true);
1342        assert_eq!("backordered", variant("lowercase"));
1343        assert_eq!("backOrdered", variant("camelCase"));
1344        assert_eq!("back_ordered", variant("snake_case"));
1345        assert_eq!("BACK-ORDERED", variant("SCREAMING-KEBAB-CASE"));
1346        assert_eq!("BackOrdered", variant("PascalCase"));
1347        assert_eq!("as_is", rename_with("as_is", None, false));
1348        assert_eq!("", rename_with("a_b", Some("Train-Case"), false));
1349    }
1350}