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/// | `#[timestamps]` | `save` sets the `created_at` and `updated_at` fields' columns (17.31.0) |
690/// | `#[soft_delete]` | `delete` sets the `deleted_at` field's column (an `Option`); queries skip such rows (17.31.0) |
691///
692/// # Field attributes
693///
694/// | Attribute | Meaning |
695/// |---|---|
696/// | `#[primary_key]` | Mark as primary key (`i64`, `i32`, `String` or `Uuid`) |
697/// | `#[primary_key(auto_increment)]` | Auto-increment primary key |
698/// | `#[column(name = "col")]` | Override column name |
699/// | `#[column(unique)]` | Mark column unique (informational) |
700/// | `#[version]` | Optimistic-locking version (an integer), checked and incremented by `save` (17.31.0) |
701/// | `#[ignore]` | Exclude from DB mapping |
702///
703/// `T::repository(&pool)` is keyed by `i64` for an integer primary key of
704/// any width (as before 17.31.0) and by the field's type otherwise
705/// (`String`, `Uuid`).
706///
707/// # Example
708///
709/// ```ignore
710/// #[derive(rust_web_server::Model, Debug, Clone)]
711/// #[table(name = "users")]
712/// pub struct User {
713///     #[primary_key(auto_increment)]
714///     pub id: i64,
715///     #[column(name = "first_name")]
716///     pub name: String,
717///     pub email: String,
718///     #[ignore]
719///     pub display_label: String,
720/// }
721///
722/// // Conventions the repository maintains (17.31.0):
723/// #[derive(rust_web_server::Model, Debug, Clone)]
724/// #[table(name = "posts")]
725/// #[timestamps]
726/// #[soft_delete]
727/// pub struct Post {
728///     #[primary_key]
729///     pub id: rust_web_server::model::Uuid, // Uuid::NIL → save picks a v7
730///     pub title: String,
731///     #[version]
732///     pub lock_version: i64,
733///     pub created_at: rust_web_server::model::Timestamp,
734///     pub updated_at: rust_web_server::model::Timestamp,
735///     pub deleted_at: Option<rust_web_server::model::Timestamp>,
736/// }
737/// ```
738#[proc_macro_derive(Model, attributes(table, column, primary_key, timestamps, soft_delete, version))]
739pub fn derive_model(input: TokenStream) -> TokenStream {
740    let ast = parse_macro_input!(input as DeriveInput);
741    impl_model(ast)
742        .unwrap_or_else(|e| e.to_compile_error())
743        .into()
744}
745
746fn impl_model(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
747    let span = input.ident.span();
748    let struct_name = &input.ident;
749
750    // ── Determine table name ──────────────────────────────────────────────────
751    let table_name = parse_table_name(&input.attrs, struct_name)?;
752
753    let fields = match &input.data {
754        Data::Struct(s) => match &s.fields {
755            Fields::Named(f) => &f.named,
756            _ => {
757                return Err(syn::Error::new(
758                    span,
759                    "#[derive(Model)] only supports structs with named fields",
760                ))
761            }
762        },
763        _ => {
764            return Err(syn::Error::new(
765                span,
766                "#[derive(Model)] can only be derived on structs",
767            ))
768        }
769    };
770
771    // ── Parse field metadata ──────────────────────────────────────────────────
772    struct FieldMeta {
773        field_ident: syn::Ident,
774        col_name: String,
775        #[allow(dead_code)]
776        is_pk: bool,
777        is_auto_increment: bool,
778        is_ignored: bool,
779        is_version: bool,
780        ty: syn::Type,
781    }
782
783    let mut pk_field: Option<FieldMeta> = None;
784    let mut regular_fields: Vec<FieldMeta> = Vec::new();
785
786    for field in fields {
787        let ident = field.ident.as_ref().unwrap().clone();
788        let mut col_name = ident.to_string();
789        let mut is_pk = false;
790        let mut is_auto_increment = false;
791        let mut is_ignored = false;
792        let mut is_version = false;
793
794        for attr in &field.attrs {
795            if attr.path().is_ident("ignore") {
796                is_ignored = true;
797            } else if attr.path().is_ident("version") {
798                is_version = true;
799            } else if attr.path().is_ident("primary_key") {
800                is_pk = true;
801                // Check for (auto_increment)
802                let _ = attr.parse_nested_meta(|meta| {
803                    if meta.path.is_ident("auto_increment") {
804                        is_auto_increment = true;
805                    }
806                    Ok(())
807                });
808            } else if attr.path().is_ident("column") {
809                let _ = attr.parse_nested_meta(|meta| {
810                    if meta.path.is_ident("name") {
811                        let lit: LitStr = meta.value()?.parse()?;
812                        col_name = lit.value();
813                    }
814                    // unique / nullable are informational — ignore
815                    Ok(())
816                });
817            }
818        }
819
820        let meta = FieldMeta {
821            field_ident: ident,
822            col_name,
823            is_pk,
824            is_auto_increment,
825            is_ignored,
826            is_version,
827            ty: field.ty.clone(),
828        };
829
830        if is_version && (is_pk || is_ignored) {
831            return Err(syn::Error::new_spanned(field, "#[version] goes on a mapped integer field, not the #[primary_key] or an #[ignore]d one"));
832        }
833        if is_pk {
834            pk_field = Some(meta);
835        } else {
836            regular_fields.push(meta);
837        }
838    }
839
840    let pk = pk_field.ok_or_else(|| {
841        syn::Error::new(span, "#[derive(Model)] requires exactly one #[primary_key] field")
842    })?;
843
844    // ── Conventions: #[timestamps], #[soft_delete], #[version] ───────────────
845    let has_struct_attr = |name: &str| input.attrs.iter().any(|a| a.path().is_ident(name));
846    let column_of = |field: &str, attr: &str, must_be_option: bool| -> syn::Result<String> {
847        let found = regular_fields.iter().find(|f| f.field_ident == field && !f.is_ignored).ok_or_else(|| {
848            syn::Error::new(span, format!("#[{attr}] needs a mapped field named `{field}` (e.g. `{field}: {}`)", if must_be_option { "Option<Timestamp>" } else { "Timestamp" }))
849        })?;
850        if must_be_option && !is_option_type(&found.ty) {
851            return Err(syn::Error::new_spanned(&found.ty, format!("#[{attr}]: `{field}` must be an Option (NULL means not deleted), e.g. Option<Timestamp>")));
852        }
853        Ok(found.col_name.clone())
854    };
855    // Only the conventions in use become methods: a model without them
856    // expands exactly as before 17.31.0, so it still compiles against a
857    // rust-web-server whose `Model` trait predates them.
858    let opt = |name: &str, c: Option<String>| -> proc_macro2::TokenStream {
859        let name = syn::Ident::new(name, proc_macro2::Span::call_site());
860        match c {
861            Some(c) => quote! {
862                fn #name() -> ::core::option::Option<&'static str> {
863                    ::core::option::Option::Some(#c)
864                }
865            },
866            None => quote! {},
867        }
868    };
869    let (created_at, updated_at) = if has_struct_attr("timestamps") {
870        (Some(column_of("created_at", "timestamps", false)?), Some(column_of("updated_at", "timestamps", false)?))
871    } else {
872        (None, None)
873    };
874    let deleted_at = if has_struct_attr("soft_delete") { Some(column_of("deleted_at", "soft_delete", true)?) } else { None };
875    let versions: Vec<&FieldMeta> = regular_fields.iter().filter(|f| f.is_version).collect();
876    if versions.len() > 1 {
877        return Err(syn::Error::new(span, "#[derive(Model)] takes at most one #[version] field"));
878    }
879    let version = versions.first().map(|f| f.col_name.clone());
880    let (created_at, updated_at, deleted_at, version) = (
881        opt("created_at_column", created_at),
882        opt("updated_at_column", updated_at),
883        opt("soft_delete_column", deleted_at),
884        opt("version_column", version),
885    );
886
887    // `repository()`'s key: i64 for any integer key (as before), else the
888    // key's own type.
889    let pk_ty = &pk.ty;
890    let is_integer = matches!(pk_ty, syn::Type::Path(tp) if tp.qself.is_none() && tp.path.segments.len() == 1
891        && ["i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize"].iter().any(|t| tp.path.segments[0].ident == t));
892    let repo_key = if is_integer { quote! { i64 } } else { quote! { #pk_ty } };
893
894    // ── column_names (all mapped fields, including PK) ────────────────────────
895    let mut all_col_names: Vec<String> = Vec::new();
896    all_col_names.push(pk.col_name.clone());
897    for f in regular_fields.iter().filter(|f| !f.is_ignored) {
898        all_col_names.push(f.col_name.clone());
899    }
900
901    let pk_col_name = pk.col_name.clone();
902    let pk_field_ident = &pk.field_ident;
903    let auto_inc = pk.is_auto_increment;
904
905    // ── from_row body ─────────────────────────────────────────────────────────
906    let pk_from_row = {
907        let col = &pk.col_name;
908        let fident = &pk.field_ident;
909        quote! { #fident: row.get(#col)? }
910    };
911
912    let regular_from_row: Vec<proc_macro2::TokenStream> = fields
913        .iter()
914        .filter(|f| {
915            let ident_str = f.ident.as_ref().unwrap().to_string();
916            // Skip the PK field (handled separately) and ignored fields.
917            let is_pk_field = ident_str == pk_field_ident.to_string();
918            !is_pk_field
919        })
920        .map(|f| {
921            let fident = f.ident.as_ref().unwrap();
922            // Check if ignored
923            let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
924            if is_ignored {
925                quote! { #fident: ::core::default::Default::default() }
926            } else {
927                // Determine column name override
928                let mut col = fident.to_string();
929                for attr in &f.attrs {
930                    if attr.path().is_ident("column") {
931                        let _ = attr.parse_nested_meta(|meta| {
932                            if meta.path.is_ident("name") {
933                                if let Ok(lit) = meta.value()?.parse::<LitStr>() {
934                                    col = lit.value();
935                                }
936                            }
937                            Ok(())
938                        });
939                    }
940                }
941                quote! { #fident: row.get(#col)? }
942            }
943        })
944        .collect();
945
946    // ── to_values body ────────────────────────────────────────────────────────
947    let pk_to_values = {
948        let col = &pk.col_name;
949        quote! {
950            (#col, ::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident))
951        }
952    };
953
954    let regular_to_values: Vec<proc_macro2::TokenStream> = fields
955        .iter()
956        .filter(|f| {
957            let ident_str = f.ident.as_ref().unwrap().to_string();
958            let is_pk_field = ident_str == pk_field_ident.to_string();
959            let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
960            !is_pk_field && !is_ignored
961        })
962        .map(|f| {
963            let fident = f.ident.as_ref().unwrap();
964            let mut col = fident.to_string();
965            for attr in &f.attrs {
966                if attr.path().is_ident("column") {
967                    let _ = attr.parse_nested_meta(|meta| {
968                        if meta.path.is_ident("name") {
969                            if let Ok(lit) = meta.value()?.parse::<LitStr>() {
970                                col = lit.value();
971                            }
972                        }
973                        Ok(())
974                    });
975                }
976            }
977            quote! {
978                (#col, ::rust_web_server::model::ToColumn::to_column(&self.#fident))
979            }
980        })
981        .collect();
982
983    // ── Generate impl ─────────────────────────────────────────────────────────
984    Ok(quote! {
985        impl ::rust_web_server::model::Model for #struct_name {
986            fn table_name() -> &'static str {
987                #table_name
988            }
989
990            fn column_names() -> &'static [&'static str] {
991                &[#(#all_col_names),*]
992            }
993
994            fn primary_key_name() -> &'static str {
995                #pk_col_name
996            }
997
998            fn primary_key_value(&self) -> ::rust_web_server::model::Value {
999                ::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident)
1000            }
1001
1002            fn primary_key_auto_increment() -> bool {
1003                #auto_inc
1004            }
1005
1006            fn from_row(row: &::rust_web_server::model::ModelRow) -> ::core::result::Result<Self, ::rust_web_server::model::DbError> {
1007                ::core::result::Result::Ok(#struct_name {
1008                    #pk_from_row,
1009                    #(#regular_from_row),*
1010                })
1011            }
1012
1013            fn to_values(&self) -> ::std::vec::Vec<(&'static str, ::rust_web_server::model::Value)> {
1014                vec![
1015                    #pk_to_values,
1016                    #(#regular_to_values),*
1017                ]
1018            }
1019
1020            #created_at
1021            #updated_at
1022            #deleted_at
1023            #version
1024        }
1025
1026        impl #struct_name {
1027            /// Create a `ModelRepository` tied to the given pool.
1028            pub fn repository(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::ModelRepository<#struct_name, #repo_key> {
1029                ::rust_web_server::model::ModelRepository::new(pool)
1030            }
1031
1032            /// Create a `QueryBuilder` tied to the given pool.
1033            pub fn query(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::QueryBuilder<#struct_name> {
1034                ::rust_web_server::model::QueryBuilder::new(pool)
1035            }
1036        }
1037    })
1038}
1039
1040fn parse_table_name(
1041    attrs: &[syn::Attribute],
1042    struct_name: &syn::Ident,
1043) -> syn::Result<String> {
1044    for attr in attrs {
1045        if attr.path().is_ident("table") {
1046            let mut name: Option<String> = None;
1047            attr.parse_nested_meta(|meta| {
1048                if meta.path.is_ident("name") {
1049                    let lit: LitStr = meta.value()?.parse()?;
1050                    name = Some(lit.value());
1051                }
1052                Ok(())
1053            })?;
1054            if let Some(n) = name {
1055                return Ok(n);
1056            }
1057        }
1058    }
1059    // Default: lowercased struct name.
1060    Ok(struct_name.to_string().to_lowercase())
1061}
1062
1063// ── #[derive(Factory)] ────────────────────────────────────────────────────────
1064
1065/// Derive `rust_web_server::test_client::factory::Factory` for a
1066/// `#[derive(Model)]` struct, and an inherent `T::factory()` returning a
1067/// `ModelFactory<T>` for tests (17.30.0).
1068///
1069/// Each field gets its type's `FactoryValue` default (numbers `0`, `bool`
1070/// `false`, `Option` `None`, a `String` the field's name plus a sequence
1071/// number), or `Default::default()` for a type without one and for
1072/// `#[ignore]` fields. The `#[primary_key]` is `0` when it's
1073/// `auto_increment`, else the sequence number. It reads the same attributes
1074/// as `#[derive(Model)]` and declares none of its own.
1075///
1076/// ```ignore
1077/// #[derive(rust_web_server::Model, rust_web_server::Factory, Debug, Clone)]
1078/// #[table(name = "users")]
1079/// pub struct User {
1080///     #[primary_key(auto_increment)]
1081///     pub id: i64,
1082///     pub name: String,
1083/// }
1084///
1085/// let user = User::factory().with(|u| u.name = "Ann".into()).create(&pool).await?;
1086/// ```
1087#[proc_macro_derive(Factory)]
1088pub fn derive_factory(input: TokenStream) -> TokenStream {
1089    let ast = parse_macro_input!(input as DeriveInput);
1090    impl_factory(ast)
1091        .unwrap_or_else(|e| e.to_compile_error())
1092        .into()
1093}
1094
1095fn impl_factory(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1096    let span = input.ident.span();
1097    let struct_name = &input.ident;
1098    if !input.generics.params.is_empty() {
1099        return Err(syn::Error::new(span, "#[derive(Factory)] doesn't support generic structs"));
1100    }
1101    let fields = match &input.data {
1102        Data::Struct(s) => match &s.fields {
1103            Fields::Named(f) => &f.named,
1104            _ => return Err(syn::Error::new(span, "#[derive(Factory)] only supports structs with named fields")),
1105        },
1106        _ => return Err(syn::Error::new(span, "#[derive(Factory)] can only be derived on structs")),
1107    };
1108
1109    let mut has_pk = false;
1110    let mut inits: Vec<proc_macro2::TokenStream> = Vec::new();
1111    for field in fields {
1112        let ident = field.ident.as_ref().unwrap();
1113        let ty = &field.ty;
1114        let name = ident.to_string();
1115        let name = name.strip_prefix("r#").unwrap_or(&name).to_string();
1116        let mut is_pk = false;
1117        let mut auto_increment = false;
1118        let mut ignored = false;
1119        for attr in &field.attrs {
1120            if attr.path().is_ident("ignore") {
1121                ignored = true;
1122            } else if attr.path().is_ident("primary_key") {
1123                is_pk = true;
1124                let _ = attr.parse_nested_meta(|meta| {
1125                    if meta.path.is_ident("auto_increment") {
1126                        auto_increment = true;
1127                    }
1128                    Ok(())
1129                });
1130            }
1131        }
1132        has_pk |= is_pk;
1133        let probe = quote! { (&&__Probe::<#ty>(::core::marker::PhantomData)) };
1134        inits.push(if ignored {
1135            quote! { #ident: ::core::default::Default::default() }
1136        } else if is_pk && !auto_increment {
1137            quote! { #ident: #probe.make_key(#name, __seq) }
1138        } else {
1139            quote! { #ident: #probe.make(#name, __seq) }
1140        });
1141    }
1142    if !has_pk {
1143        return Err(syn::Error::new(span, "#[derive(Factory)] needs the #[primary_key] field of #[derive(Model)]"));
1144    }
1145
1146    Ok(quote! {
1147        impl ::rust_web_server::test_client::factory::Factory for #struct_name {
1148            fn factory_default(__seq: u64) -> Self {
1149                #[allow(unused_imports)]
1150                use ::rust_web_server::test_client::factory::__private::{
1151                    Probe as __Probe, ViaDefault as _, ViaValue as _,
1152                };
1153                #struct_name {
1154                    #(#inits),*
1155                }
1156            }
1157        }
1158
1159        impl #struct_name {
1160            /// A factory building this model with default values, for tests.
1161            pub fn factory() -> ::rust_web_server::test_client::factory::ModelFactory<#struct_name> {
1162                ::rust_web_server::test_client::factory::ModelFactory::new()
1163            }
1164        }
1165    })
1166}
1167
1168// ── #[derive(JsonSchema)] ─────────────────────────────────────────────────────
1169
1170/// Derive `rust_web_server::schema::JsonSchema` for a struct with named
1171/// fields or an enum of unit variants.
1172///
1173/// The schema follows the type's serde attributes, so it describes the JSON
1174/// `serde_json` reads and writes: container `rename_all` and `default`;
1175/// field `rename`, `default`, `skip`/`skip_serializing`/`skip_deserializing`
1176/// (left out) and `skip_serializing_if` (not required); variant `rename`.
1177/// Attributes that change the JSON's shape — `flatten`, `tag`, `content`,
1178/// `untagged`, `transparent`, `with`/`serialize_with`/`deserialize_with`,
1179/// separate serialize/deserialize renames — are a compile error rather than a
1180/// schema that doesn't match. `Option` fields are not required. Doc comments
1181/// become `description`s.
1182///
1183/// ```ignore
1184/// #[derive(serde::Serialize, serde::Deserialize, rust_web_server::JsonSchema)]
1185/// #[serde(rename_all = "camelCase")]
1186/// struct Item {
1187///     /// Stock keeping unit.
1188///     sku: String,
1189///     in_stock: u32,
1190///     note: Option<String>,
1191/// }
1192/// ```
1193#[proc_macro_derive(JsonSchema, attributes(serde))]
1194pub fn derive_json_schema(input: TokenStream) -> TokenStream {
1195    let ast = parse_macro_input!(input as DeriveInput);
1196    impl_json_schema(ast)
1197        .unwrap_or_else(|e| e.to_compile_error())
1198        .into()
1199}
1200
1201fn impl_json_schema(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1202    let name = &input.ident;
1203    let container = SerdeAttrs::parse(&input.attrs)?;
1204    let description = doc_comment(&input.attrs);
1205    let body = match &input.data {
1206        Data::Struct(s) => {
1207            let Fields::Named(fields) = &s.fields else {
1208                return Err(syn::Error::new_spanned(
1209                    name,
1210                    "#[derive(JsonSchema)] supports structs with named fields and enums of unit variants",
1211                ));
1212            };
1213            let mut properties = Vec::new();
1214            for field in &fields.named {
1215                let attrs = SerdeAttrs::parse(&field.attrs)?;
1216                if attrs.skip {
1217                    continue;
1218                }
1219                let ident = field.ident.as_ref().expect("named field");
1220                let json_name = attrs.rename.clone().unwrap_or_else(|| {
1221                    rename_with(ident.to_string().trim_start_matches("r#"), container.rename_all.as_deref(), false)
1222                });
1223                let ty = &field.ty;
1224                let field_doc = doc_comment(&field.attrs);
1225                let may_be_absent = attrs.default || container.default || attrs.skip_serializing_if;
1226                properties.push(quote! {
1227                    __schema.property(
1228                        #json_name,
1229                        &_rws::schema::nested::<#ty>(),
1230                        #field_doc,
1231                        !#may_be_absent && !<#ty as _rws::schema::JsonSchema>::is_optional(),
1232                    );
1233                });
1234            }
1235            quote! {
1236                let mut __schema = _rws::schema::ObjectSchema::new(#description);
1237                #(#properties)*
1238                __schema.finish()
1239            }
1240        }
1241        Data::Enum(e) => {
1242            let mut values = Vec::new();
1243            for variant in &e.variants {
1244                if !matches!(variant.fields, Fields::Unit) {
1245                    return Err(syn::Error::new_spanned(
1246                        variant,
1247                        "#[derive(JsonSchema)] supports only unit enum variants",
1248                    ));
1249                }
1250                let attrs = SerdeAttrs::parse(&variant.attrs)?;
1251                if attrs.skip {
1252                    continue;
1253                }
1254                values.push(attrs.rename.unwrap_or_else(|| {
1255                    rename_with(&variant.ident.to_string(), container.rename_all.as_deref(), true)
1256                }));
1257            }
1258            quote! { _rws::schema::string_enum(#description, &[#(#values),*]) }
1259        }
1260        Data::Union(_) => {
1261            return Err(syn::Error::new_spanned(name, "#[derive(JsonSchema)] does not support unions"));
1262        }
1263    };
1264
1265    let mut generics = input.generics.clone();
1266    for param in generics.type_params_mut() {
1267        param.bounds.push(syn::parse_quote!(::rust_web_server::schema::JsonSchema));
1268    }
1269    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1270    Ok(quote! {
1271        const _: () = {
1272            use ::rust_web_server as _rws;
1273            impl #impl_generics _rws::schema::JsonSchema for #name #ty_generics #where_clause {
1274                fn json_schema() -> ::std::string::String {
1275                    #body
1276                }
1277            }
1278        };
1279    })
1280}
1281
1282/// The `#[serde(...)]` attributes `#[derive(JsonSchema)]` understands.
1283#[derive(Default)]
1284struct SerdeAttrs {
1285    rename: Option<String>,
1286    rename_all: Option<String>,
1287    default: bool,
1288    skip: bool,
1289    skip_serializing_if: bool,
1290}
1291
1292impl SerdeAttrs {
1293    fn parse(attrs: &[syn::Attribute]) -> syn::Result<Self> {
1294        let mut out = SerdeAttrs::default();
1295        for attr in attrs.iter().filter(|a| a.path().is_ident("serde")) {
1296            attr.parse_nested_meta(|meta| {
1297                let key = meta.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
1298                match key.as_str() {
1299                    "rename" | "rename_all" => {
1300                        if !meta.input.peek(Token![=]) {
1301                            return Err(meta.error(format!(
1302                                "#[derive(JsonSchema)] needs one `{key}` for both directions (one schema describes input and output)"
1303                            )));
1304                        }
1305                        let value = meta.value()?.parse::<LitStr>()?.value();
1306                        if key == "rename" {
1307                            out.rename = Some(value);
1308                        } else {
1309                            if rename_with("a_b", Some(&value), false).is_empty() {
1310                                return Err(meta.error(format!("unknown rename_all rule \"{value}\"")));
1311                            }
1312                            out.rename_all = Some(value);
1313                        }
1314                    }
1315                    "default" => {
1316                        out.default = true;
1317                        if meta.input.peek(Token![=]) {
1318                            meta.value()?.parse::<LitStr>()?;
1319                        }
1320                    }
1321                    "skip" | "skip_serializing" | "skip_deserializing" => out.skip = true,
1322                    "skip_serializing_if" => {
1323                        out.skip_serializing_if = true;
1324                        meta.value()?.parse::<LitStr>()?;
1325                    }
1326                    "flatten" | "tag" | "content" | "untagged" | "transparent" | "with" | "serialize_with"
1327                    | "deserialize_with" | "remote" | "from" | "try_from" | "into" | "getter" => {
1328                        return Err(meta.error(format!(
1329                            "#[derive(JsonSchema)] does not support #[serde({key})]: the schema would not match the JSON"
1330                        )));
1331                    }
1332                    _ => {
1333                        // Attributes that don't change the JSON's shape
1334                        // (alias, deny_unknown_fields, bound, crate, borrow,
1335                        // expecting, ...): skip their value, if any.
1336                        if meta.input.peek(Token![=]) {
1337                            meta.value()?.parse::<proc_macro2::TokenStream>()?;
1338                        } else if meta.input.peek(syn::token::Paren) {
1339                            meta.parse_nested_meta(|nested| {
1340                                if nested.input.peek(Token![=]) {
1341                                    nested.value()?.parse::<proc_macro2::TokenTree>()?;
1342                                }
1343                                Ok(())
1344                            })?;
1345                        }
1346                    }
1347                }
1348                Ok(())
1349            })?;
1350        }
1351        Ok(out)
1352    }
1353}
1354
1355/// `name` renamed by serde's `rename_all` `rule` (`variant`: `name` is a
1356/// PascalCase enum variant rather than a snake_case field). Empty for an
1357/// unknown rule.
1358fn rename_with(name: &str, rule: Option<&str>, variant: bool) -> String {
1359    let Some(rule) = rule else { return name.to_string() };
1360    // Split into lower-case words.
1361    let words: Vec<String> = if variant {
1362        let mut words = Vec::new();
1363        let mut current = String::new();
1364        for c in name.chars() {
1365            if c.is_uppercase() && !current.is_empty() {
1366                words.push(std::mem::take(&mut current));
1367            }
1368            current.extend(c.to_lowercase());
1369        }
1370        if !current.is_empty() {
1371            words.push(current);
1372        }
1373        words
1374    } else {
1375        name.split('_').map(str::to_string).collect()
1376    };
1377    let capitalize = |w: &String| {
1378        let mut chars = w.chars();
1379        chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
1380    };
1381    match rule {
1382        "lowercase" => words.concat(),
1383        "UPPERCASE" => words.concat().to_uppercase(),
1384        "PascalCase" => words.iter().map(capitalize).collect(),
1385        "camelCase" => {
1386            let pascal: String = words.iter().map(capitalize).collect();
1387            let mut chars = pascal.chars();
1388            chars.next().map(|c| c.to_lowercase().chain(chars).collect()).unwrap_or_default()
1389        }
1390        "snake_case" => words.join("_"),
1391        "SCREAMING_SNAKE_CASE" => words.join("_").to_uppercase(),
1392        "kebab-case" => words.join("-"),
1393        "SCREAMING-KEBAB-CASE" => words.join("-").to_uppercase(),
1394        _ => String::new(),
1395    }
1396}
1397
1398/// The item's `///` doc comment, lines joined with spaces.
1399fn doc_comment(attrs: &[syn::Attribute]) -> String {
1400    let mut lines = Vec::new();
1401    for attr in attrs.iter().filter(|a| a.path().is_ident("doc")) {
1402        if let syn::Meta::NameValue(nv) = &attr.meta {
1403            if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value {
1404                let line = s.value().trim().to_string();
1405                if !line.is_empty() {
1406                    lines.push(line);
1407                }
1408            }
1409        }
1410    }
1411    lines.join(" ")
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416    use super::rename_with;
1417
1418    #[test]
1419    fn rename_all_rules_match_serde() {
1420        let field = |rule| rename_with("line_item_id", Some(rule), false);
1421        assert_eq!("lineitemid", field("lowercase"));
1422        assert_eq!("LINEITEMID", field("UPPERCASE"));
1423        assert_eq!("LineItemId", field("PascalCase"));
1424        assert_eq!("lineItemId", field("camelCase"));
1425        assert_eq!("line_item_id", field("snake_case"));
1426        assert_eq!("LINE_ITEM_ID", field("SCREAMING_SNAKE_CASE"));
1427        assert_eq!("line-item-id", field("kebab-case"));
1428        assert_eq!("LINE-ITEM-ID", field("SCREAMING-KEBAB-CASE"));
1429        let variant = |rule| rename_with("BackOrdered", Some(rule), true);
1430        assert_eq!("backordered", variant("lowercase"));
1431        assert_eq!("backOrdered", variant("camelCase"));
1432        assert_eq!("back_ordered", variant("snake_case"));
1433        assert_eq!("BACK-ORDERED", variant("SCREAMING-KEBAB-CASE"));
1434        assert_eq!("BackOrdered", variant("PascalCase"));
1435        assert_eq!("as_is", rename_with("as_is", None, false));
1436        assert_eq!("", rename_with("a_b", Some("Train-Case"), false));
1437    }
1438}