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