Skip to main content

blazingly_macros/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt::Write as _;
5use proc_macro::TokenStream;
6use quote::{format_ident, quote};
7use syn::parse::{Parse, ParseStream};
8use syn::{
9    Attribute, Fields, FnArg, Ident, ItemEnum, ItemFn, ItemStruct, LitBool, LitInt, LitStr, Pat,
10    PatType, Path as SynPath, ReturnType, Token, Type, TypePath, bracketed, parse_macro_input,
11};
12
13struct OperationArgs {
14    path: LitStr,
15    id: LitStr,
16    summary: LitStr,
17}
18
19struct UniversalOperationArgs {
20    method: HttpMethodArgument,
21    operation: OperationArgs,
22}
23
24struct HttpMethodArgument {
25    value: String,
26    span: proc_macro2::Span,
27}
28
29#[derive(Clone, Copy)]
30enum ProviderLifetimeArgument {
31    Singleton,
32    Request,
33    Transient,
34}
35
36struct ProviderArgs {
37    lifetime: ProviderLifetimeArgument,
38}
39
40struct SecurityArgs {
41    scheme: LitStr,
42    scopes: Vec<LitStr>,
43}
44
45#[derive(Default)]
46struct McpArgs {
47    name: Option<LitStr>,
48    description: Option<LitStr>,
49    risk: Option<LitStr>,
50    confirmation: Option<LitStr>,
51    idempotent: Option<LitBool>,
52    expose_output: Option<LitStr>,
53}
54
55#[derive(Default)]
56struct ModelArgs {
57    rename_all: Option<LitStr>,
58    validator: Option<SynPath>,
59    /// Present when `#[api_model(borrowed)]` selected the output-only form.
60    borrowed: Option<Ident>,
61}
62
63impl Parse for ModelArgs {
64    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
65        let mut arguments = Self::default();
66
67        while !input.is_empty() {
68            let key = input.parse::<Ident>()?;
69
70            match key.to_string().as_str() {
71                // A bare flag, not a `key = value` pair: a view either borrows
72                // or it does not.
73                "borrowed" => {
74                    if arguments.borrowed.is_some() {
75                        return Err(syn::Error::new(
76                            key.span(),
77                            "`borrowed` was specified twice",
78                        ));
79                    }
80                    arguments.borrowed = Some(key);
81                }
82                "rename_all" => {
83                    input.parse::<Token![=]>()?;
84                    if arguments.rename_all.is_some() {
85                        return Err(syn::Error::new(
86                            key.span(),
87                            "`rename_all` was specified twice",
88                        ));
89                    }
90                    arguments.rename_all = Some(input.parse::<LitStr>()?);
91                }
92                "validate_with" => {
93                    input.parse::<Token![=]>()?;
94                    if arguments.validator.is_some() {
95                        return Err(syn::Error::new(
96                            key.span(),
97                            "only one model `validate_with` function may be declared",
98                        ));
99                    }
100                    arguments.validator = Some(input.parse::<SynPath>()?);
101                }
102                _ => {
103                    return Err(syn::Error::new(
104                        key.span(),
105                        "the supported model options are `borrowed`, `rename_all`, \
106                         and `validate_with`",
107                    ));
108                }
109            }
110
111            if !input.is_empty() {
112                input.parse::<Token![,]>()?;
113            }
114        }
115
116        Ok(arguments)
117    }
118}
119
120/// A declarative numeric bound written as an attribute literal.
121#[derive(Clone, Copy, Debug, PartialEq)]
122enum NumericLiteral {
123    Integer(i128),
124    Float(f64),
125}
126
127impl NumericLiteral {
128    fn tokens(self) -> proc_macro2::TokenStream {
129        match self {
130            Self::Integer(value) => {
131                let literal = proc_macro2::Literal::i128_suffixed(value);
132                quote!(::blazingly::validation::NumericValue::Integer(#literal))
133            }
134            Self::Float(value) => {
135                let literal = proc_macro2::Literal::f64_suffixed(value);
136                quote!(::blazingly::validation::NumericValue::Float(#literal))
137            }
138        }
139    }
140
141    /// The bare literal, so the field type it is written for infers itself.
142    fn literal_tokens(self) -> proc_macro2::TokenStream {
143        match self {
144            Self::Integer(value) => {
145                let literal = proc_macro2::Literal::i128_unsuffixed(value);
146                quote!(#literal)
147            }
148            Self::Float(value) => {
149                let literal = proc_macro2::Literal::f64_unsuffixed(value);
150                quote!(#literal)
151            }
152        }
153    }
154
155    fn encoded(self) -> String {
156        match self {
157            Self::Integer(value) => value.to_string(),
158            Self::Float(value) if value.fract() == 0.0 => format!("{value:.1}"),
159            Self::Float(value) => value.to_string(),
160        }
161    }
162
163    // Comparing a mixed integer and float pair follows JSON Schema semantics.
164    #[allow(clippy::cast_precision_loss)]
165    fn widened(self) -> f64 {
166        match self {
167            Self::Integer(value) => value as f64,
168            Self::Float(value) => value,
169        }
170    }
171
172    fn exceeds(self, other: Self) -> bool {
173        if let (Self::Integer(left), Self::Integer(right)) = (self, other) {
174            return left > right;
175        }
176        self.widened() > other.widened()
177    }
178
179    fn is_zero(self) -> bool {
180        match self {
181            Self::Integer(value) => value == 0,
182            Self::Float(value) => value == 0.0,
183        }
184    }
185}
186
187/// A field default written as an attribute literal.
188///
189/// The literal is emitted twice: once as the body of the serde default so the
190/// handler never sees an absent field, and once as JSON in the descriptor so a
191/// schema projection can state the value a client may omit.
192#[derive(Clone)]
193enum DefaultLiteral {
194    Text(LitStr),
195    Number(NumericLiteral),
196    Boolean(LitBool),
197}
198
199impl DefaultLiteral {
200    fn expression(&self) -> proc_macro2::TokenStream {
201        match self {
202            Self::Text(value) => quote!(::std::string::String::from(#value)),
203            Self::Number(value) => value.literal_tokens(),
204            Self::Boolean(value) => quote!(#value),
205        }
206    }
207
208    /// The JSON form recorded in the descriptor.
209    fn encoded(&self) -> String {
210        match self {
211            Self::Text(value) => json_string(&value.value()),
212            Self::Number(value) => value.encoded(),
213            Self::Boolean(value) => value.value().to_string(),
214        }
215    }
216
217    /// What the literal is, and the field it can be written on.
218    const fn expectation(&self) -> (&'static str, &'static str) {
219        match self {
220            Self::Text(_) => ("a string literal", "a `String` field"),
221            Self::Number(NumericLiteral::Integer(_)) => {
222                ("an integer literal", "an integer or floating-point field")
223            }
224            Self::Number(NumericLiteral::Float(_)) => {
225                ("a floating-point literal", "a floating-point field")
226            }
227            Self::Boolean(_) => ("a boolean literal", "a `bool` field"),
228        }
229    }
230}
231
232/// Encodes a Rust string as a JSON string literal.
233fn json_string(value: &str) -> String {
234    let mut encoded = String::with_capacity(value.len() + 2);
235    encoded.push('"');
236    for character in value.chars() {
237        match character {
238            '"' => encoded.push_str("\\\""),
239            '\\' => encoded.push_str("\\\\"),
240            '\n' => encoded.push_str("\\n"),
241            '\r' => encoded.push_str("\\r"),
242            '\t' => encoded.push_str("\\t"),
243            control if control < ' ' => {
244                let _ = write!(encoded, "\\u{:04x}", control as u32);
245            }
246            other => encoded.push(other),
247        }
248    }
249    encoded.push('"');
250    encoded
251}
252
253/// The syntactic value shape a field's declarative rules are checked against.
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255enum FieldShape {
256    Text,
257    Integer,
258    Float,
259    Collection,
260    Other,
261}
262
263impl FieldShape {
264    const fn is_numeric(self) -> bool {
265        matches!(self, Self::Integer | Self::Float)
266    }
267
268    const fn may_be_model(self) -> bool {
269        matches!(self, Self::Collection | Self::Other)
270    }
271}
272
273#[derive(Default)]
274struct FieldRules {
275    min_length: Option<(usize, proc_macro2::Span)>,
276    max_length: Option<(usize, proc_macro2::Span)>,
277    email: Option<proc_macro2::Span>,
278    aliases: Vec<LitStr>,
279    validator: Option<SynPath>,
280    nested: bool,
281    minimum: Option<(NumericLiteral, proc_macro2::Span)>,
282    maximum: Option<(NumericLiteral, proc_macro2::Span)>,
283    exclusive_minimum: Option<(NumericLiteral, proc_macro2::Span)>,
284    exclusive_maximum: Option<(NumericLiteral, proc_macro2::Span)>,
285    multiple_of: Option<(NumericLiteral, proc_macro2::Span)>,
286    pattern: Option<LitStr>,
287    min_items: Option<(usize, proc_macro2::Span)>,
288    max_items: Option<(usize, proc_macro2::Span)>,
289    unique_items: Option<proc_macro2::Span>,
290    default: Option<(DefaultLiteral, proc_macro2::Span)>,
291}
292
293struct OperationOutput {
294    status: u16,
295    success: Option<Type>,
296    error: Option<Type>,
297}
298
299#[derive(Clone, Copy)]
300enum OperationInputKind {
301    Path,
302    Query,
303    Header,
304    Cookie,
305    Json,
306    Form,
307    Multipart,
308    File,
309    Stream,
310    WebSocket,
311    Extension,
312    Extract,
313    Dependency,
314    DirectDependency,
315}
316
317struct OperationInput {
318    name: LitStr,
319    kind: OperationInputKind,
320    argument_type: Type,
321    inner: Type,
322    required: bool,
323    /// Set when the handler wrote `&Depends<T>` or `&T`.
324    ///
325    /// A dependency taken by reference is what lets a handler return a borrowed
326    /// view over it: the view's lifetime is the argument's, and the response is
327    /// encoded before the argument goes out of scope.
328    by_reference: bool,
329}
330
331struct ErrorVariant {
332    status: u16,
333    code: LitStr,
334    message: LitStr,
335    identifier: Ident,
336    payload: Option<Type>,
337    headers: Vec<(LitStr, LitStr)>,
338}
339
340impl Parse for McpArgs {
341    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
342        let mut arguments = Self::default();
343
344        while !input.is_empty() {
345            let key = input.parse::<Ident>()?;
346            input.parse::<Token![=]>()?;
347
348            match key.to_string().as_str() {
349                "name" => arguments.name = Some(input.parse()?),
350                "description" => arguments.description = Some(input.parse()?),
351                "risk" => arguments.risk = Some(input.parse()?),
352                "confirmation" => arguments.confirmation = Some(input.parse()?),
353                "idempotent" => arguments.idempotent = Some(input.parse()?),
354                "expose_output" => arguments.expose_output = Some(input.parse()?),
355                _ => {
356                    return Err(syn::Error::new(
357                        key.span(),
358                        "supported MCP keys are `name`, `description`, `risk`, \
359                         `confirmation`, `idempotent`, and `expose_output`",
360                    ));
361                }
362            }
363
364            if !input.is_empty() {
365                input.parse::<Token![,]>()?;
366            }
367        }
368
369        Ok(arguments)
370    }
371}
372
373impl Parse for OperationArgs {
374    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
375        let path = input.parse::<LitStr>()?;
376        let mut id = None;
377        let mut summary = None;
378
379        while !input.is_empty() {
380            input.parse::<Token![,]>()?;
381            if input.is_empty() {
382                break;
383            }
384
385            let key = input.parse::<Ident>()?;
386            input.parse::<Token![=]>()?;
387            let value = input.parse::<LitStr>()?;
388
389            match key.to_string().as_str() {
390                "id" => id = Some(value),
391                "summary" => summary = Some(value),
392                _ => {
393                    return Err(syn::Error::new(
394                        key.span(),
395                        "supported keys are `id` and `summary`",
396                    ));
397                }
398            }
399        }
400
401        let id = id.ok_or_else(|| {
402            syn::Error::new(path.span(), "an explicit stable `id = \"...\"` is required")
403        })?;
404        let summary = summary.unwrap_or_else(|| LitStr::new("", path.span()));
405
406        Ok(Self { path, id, summary })
407    }
408}
409
410impl Parse for UniversalOperationArgs {
411    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
412        let mut method = None;
413        let mut path = None;
414        let mut id = None;
415        let mut summary = None;
416
417        while !input.is_empty() {
418            let key = input.parse::<Ident>()?;
419            input.parse::<Token![=]>()?;
420            match key.to_string().as_str() {
421                "method" => {
422                    if method.is_some() {
423                        return Err(syn::Error::new(key.span(), "`method` was specified twice"));
424                    }
425                    method = Some(if input.peek(LitStr) {
426                        let value = input.parse::<LitStr>()?;
427                        HttpMethodArgument {
428                            value: value.value(),
429                            span: value.span(),
430                        }
431                    } else {
432                        let value = input.parse::<Ident>()?;
433                        HttpMethodArgument {
434                            value: value.to_string(),
435                            span: value.span(),
436                        }
437                    });
438                }
439                "path" => {
440                    if path.is_some() {
441                        return Err(syn::Error::new(key.span(), "`path` was specified twice"));
442                    }
443                    path = Some(input.parse::<LitStr>()?);
444                }
445                "id" => {
446                    if id.is_some() {
447                        return Err(syn::Error::new(key.span(), "`id` was specified twice"));
448                    }
449                    id = Some(input.parse::<LitStr>()?);
450                }
451                "summary" => {
452                    if summary.is_some() {
453                        return Err(syn::Error::new(key.span(), "`summary` was specified twice"));
454                    }
455                    summary = Some(input.parse::<LitStr>()?);
456                }
457                _ => {
458                    return Err(syn::Error::new(
459                        key.span(),
460                        "supported keys are `method`, `path`, `id`, and `summary`",
461                    ));
462                }
463            }
464
465            if !input.is_empty() {
466                input.parse::<Token![,]>()?;
467            }
468        }
469
470        let method =
471            method.ok_or_else(|| syn::Error::new(input.span(), "`method = ...` is required"))?;
472        let path =
473            path.ok_or_else(|| syn::Error::new(input.span(), "`path = \"...\"` is required"))?;
474        let id = id.ok_or_else(|| {
475            syn::Error::new(
476                input.span(),
477                "an explicit stable `id = \"...\"` is required",
478            )
479        })?;
480        let summary = summary.unwrap_or_else(|| LitStr::new("", path.span()));
481
482        Ok(Self {
483            method,
484            operation: OperationArgs { path, id, summary },
485        })
486    }
487}
488
489impl Parse for ProviderArgs {
490    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
491        if input.is_empty() {
492            return Ok(Self {
493                lifetime: ProviderLifetimeArgument::Request,
494            });
495        }
496
497        let lifetime = input.parse::<Ident>()?;
498        if !input.is_empty() {
499            return Err(input.error(
500                "use `#[provider]`, `#[provider(singleton)]`, \
501                 `#[provider(request)]`, or `#[provider(transient)]`",
502            ));
503        }
504        let lifetime = match lifetime.to_string().as_str() {
505            "singleton" => ProviderLifetimeArgument::Singleton,
506            "request" => ProviderLifetimeArgument::Request,
507            "transient" => ProviderLifetimeArgument::Transient,
508            _ => {
509                return Err(syn::Error::new(
510                    lifetime.span(),
511                    "provider lifetime must be `singleton`, `request`, or `transient`",
512                ));
513            }
514        };
515        Ok(Self { lifetime })
516    }
517}
518
519impl Parse for SecurityArgs {
520    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
521        let scheme = input.parse::<LitStr>()?;
522        let mut scopes = Vec::new();
523        if !input.is_empty() {
524            input.parse::<Token![,]>()?;
525            let key = input.parse::<Ident>()?;
526            if key != "scopes" {
527                return Err(syn::Error::new(
528                    key.span(),
529                    "the supported security option is `scopes = [\"...\"]`",
530                ));
531            }
532            input.parse::<Token![=]>()?;
533            let content;
534            bracketed!(content in input);
535            while !content.is_empty() {
536                scopes.push(content.parse()?);
537                if !content.is_empty() {
538                    content.parse::<Token![,]>()?;
539                }
540            }
541        }
542        if !input.is_empty() {
543            return Err(input.error("unexpected security option"));
544        }
545        Ok(Self { scheme, scopes })
546    }
547}
548
549/// Defines an operation using an explicit HTTP method.
550///
551/// This is the universal form behind the method-specific operation macros:
552/// `#[operation(method = PUT, path = "/users/{id}", id = "users.replace")]`.
553#[proc_macro_attribute]
554pub fn operation(arguments: TokenStream, item: TokenStream) -> TokenStream {
555    let arguments = parse_macro_input!(arguments as UniversalOperationArgs);
556    let method = match http_method_tokens(&arguments.method) {
557        Ok(method) => method,
558        Err(error) => return error.into_compile_error().into(),
559    };
560    let mut function = parse_macro_input!(item as ItemFn);
561
562    match operation_tokens(arguments.operation, &mut function, &method) {
563        Ok(tokens) => tokens.into(),
564        Err(error) => error.into_compile_error().into(),
565    }
566}
567
568/// Declares a `GET` operation.
569///
570/// ```ignore
571/// #[get("/users/{id}", id = "users.read", summary = "Read a user")]
572/// async fn read_user(Path(id): Path<u64>) -> Json<UserView> { .. }
573/// ```
574///
575/// The path is positional and first. `id` is required and is the operation's
576/// stable identity: it names the operation in the contract, in OpenAPI, in the
577/// generated documentation, in compatibility reports, and as the MCP tool name,
578/// so it must outlive changes to the path and to the function name. `summary`
579/// is optional and supplies the one-line description that reaches the OpenAPI
580/// operation, the `cargo blazingly routes` table, and the MCP tool description.
581///
582/// Handler arguments are the extractors — `Path`, `Query`, `Header`, `Cookie`,
583/// `Json`, `Form`, `Multipart`, `File`, `Extract<T>` — and compiled dependency
584/// requests, in any combination. The return type declares the response
585/// contract. The handler may be `async` or plain `fn`; a plain `fn` runs inline
586/// on the calling thread and is never moved to the blocking pool, so work that
587/// genuinely blocks must call `run_blocking`.
588///
589/// This is an alias for [`macro@operation`] with the method fixed.
590#[proc_macro_attribute]
591pub fn get(arguments: TokenStream, item: TokenStream) -> TokenStream {
592    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Get))
593}
594
595/// Declares a `HEAD` operation. See [`macro@get`] for the argument form.
596#[proc_macro_attribute]
597pub fn head(arguments: TokenStream, item: TokenStream) -> TokenStream {
598    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Head))
599}
600
601/// Declares a `POST` operation. See [`macro@get`] for the argument form.
602///
603/// ```ignore
604/// #[post("/users", id = "users.create", summary = "Create a user")]
605/// async fn create_user(Json(input): Json<CreateUser>) -> Created<UserView> { .. }
606/// ```
607#[proc_macro_attribute]
608pub fn post(arguments: TokenStream, item: TokenStream) -> TokenStream {
609    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Post))
610}
611
612/// Declares a `PUT` operation. See [`macro@get`] for the argument form.
613#[proc_macro_attribute]
614pub fn put(arguments: TokenStream, item: TokenStream) -> TokenStream {
615    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Put))
616}
617
618/// Declares a `PATCH` operation. See [`macro@get`] for the argument form.
619#[proc_macro_attribute]
620pub fn patch(arguments: TokenStream, item: TokenStream) -> TokenStream {
621    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Patch))
622}
623
624/// Declares a `DELETE` operation. See [`macro@get`] for the argument form.
625#[proc_macro_attribute]
626pub fn delete(arguments: TokenStream, item: TokenStream) -> TokenStream {
627    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Delete))
628}
629
630/// Declares an `OPTIONS` operation. See [`macro@get`] for the argument form.
631#[proc_macro_attribute]
632pub fn options(arguments: TokenStream, item: TokenStream) -> TokenStream {
633    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Options))
634}
635
636/// Declares a `TRACE` operation. See [`macro@get`] for the argument form.
637#[proc_macro_attribute]
638pub fn trace(arguments: TokenStream, item: TokenStream) -> TokenStream {
639    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Trace))
640}
641
642/// Declares a `CONNECT` operation. See [`macro@get`] for the argument form.
643#[proc_macro_attribute]
644pub fn connect(arguments: TokenStream, item: TokenStream) -> TokenStream {
645    expand_operation(arguments, item, &quote!(::blazingly::HttpMethod::Connect))
646}
647
648/// Declares an API model.
649///
650/// The default form owns its data: it derives `Serialize` and `Deserialize`,
651/// implements `ApiModel`, and runs every declared field rule before the handler
652/// sees the value.
653///
654/// `#[api_model(borrowed)]` declares the output-only form instead. A borrowed
655/// view derives `Serialize` and implements `ApiSchema` directly; it gains no
656/// `Deserialize` impl and no validation, because a response body is produced by
657/// the operation rather than parsed from a client. Only the borrowed form may
658/// carry lifetime and type parameters, so one `Page<'store, T>` describes every
659/// paginated response instead of one envelope per item type.
660///
661/// ```ignore
662/// #[api_model(borrowed)]
663/// struct SummaryView<'store> {
664///     title: &'store str,
665///     tags: Vec<&'store TagRef>,
666/// }
667/// ```
668///
669/// A one-field tuple struct declares a *value type*: a bundle of field rules
670/// named once and applied by every field declared with it.
671///
672/// ```ignore
673/// #[api_model]
674/// #[min_length(8)]
675/// #[max_length(200)]
676/// struct Title(String);
677/// ```
678///
679/// A unit-variant enum declares a closed set of strings, schema included.
680///
681/// ```ignore
682/// #[api_model(rename_all = "lowercase")]
683/// enum Language {
684///     Uk,
685///     Ru,
686///     En,
687/// }
688/// ```
689#[proc_macro_attribute]
690pub fn api_model(arguments: TokenStream, item: TokenStream) -> TokenStream {
691    let arguments = parse_macro_input!(arguments as ModelArgs);
692    let mut model = parse_macro_input!(item as syn::Item);
693
694    match api_model_tokens(&arguments, &mut model) {
695        Ok(tokens) => tokens.into(),
696        Err(error) => error.into_compile_error().into(),
697    }
698}
699
700/// Declares a stable domain error as an enum.
701///
702/// Each variant carries its own HTTP status, a stable machine-readable code,
703/// and an optional human message; a variant may also declare response headers
704/// and a typed details payload.
705///
706/// ```ignore
707/// #[api_error]
708/// enum CreateUserError {
709///     #[status(409)]
710///     #[code("email_already_exists")]
711///     #[message("A user with this email already exists.")]
712///     EmailAlreadyExists,
713///
714///     #[status(429)]
715///     #[code("rate_limited")]
716///     #[header("retry-after", "30")]
717///     RateLimited(RateLimitDetails),
718/// }
719/// ```
720///
721/// The code is the stable identity, the same way an operation `id` is: it
722/// reaches the response body, the OpenAPI responses, the generated
723/// documentation, and MCP typed-error handling, and it participates in
724/// compatibility reports. Returning `Result<T, ThisError>` from a handler is
725/// what declares the error responses on the operation.
726///
727/// Faults the framework raises itself — an invalid response header, a
728/// serialization failure — are not projected this way. They are redacted to a
729/// generic `500` over HTTP and a generic internal error over MCP.
730#[proc_macro_attribute]
731pub fn api_error(_arguments: TokenStream, item: TokenStream) -> TokenStream {
732    let mut error = parse_macro_input!(item as ItemEnum);
733
734    match error_tokens(&mut error) {
735        Ok(tokens) => tokens.into(),
736        Err(error) => error.into_compile_error().into(),
737    }
738}
739
740/// Turns a typed factory function into a compiled DI provider declaration.
741///
742/// `#[provider]` defaults to request scope. `singleton`, `request`, and
743/// `transient` can be selected explicitly. Asyncness and a
744/// `Result<T, DependencyError>` return are inferred from the function.
745#[proc_macro_attribute]
746pub fn provider(arguments: TokenStream, item: TokenStream) -> TokenStream {
747    let arguments = parse_macro_input!(arguments as ProviderArgs);
748    let function = parse_macro_input!(item as ItemFn);
749
750    match provider_tokens(&arguments, &function) {
751        Ok(tokens) => tokens.into(),
752        Err(error) => error.into_compile_error().into(),
753    }
754}
755
756/// Exposes an operation as a native MCP tool.
757///
758/// ```ignore
759/// #[post("/users", id = "users.create", summary = "Create a user")]
760/// #[mcp::tool(
761///     name = "create_user",
762///     risk = "write",
763///     confirmation = "required",
764///     expose_output = "full"
765/// )]
766/// async fn create_user(Json(input): Json<CreateUser>) -> Created<UserView> { .. }
767/// ```
768///
769/// Every option is optional: `name` overrides the tool name (the operation `id`
770/// otherwise), `description` overrides the summary, `risk` and `confirmation`
771/// declare agent policy, and `expose_output` controls how much of the response
772/// a model may see. A tool declared `confirmation = "required"` is rejected
773/// unless the MCP host sends `_meta["dev.blazingly/confirmed"] = true` after
774/// obtaining user confirmation.
775///
776/// The tool is not a translation of the OpenAPI document: an agent calling it
777/// runs the same executor, the same validation, and the same typed errors as an
778/// HTTP client.
779///
780/// This attribute only annotates; it never expands on its own. It must sit
781/// *below* the operation attribute that owns the function, and says so with a
782/// compile error if it does not.
783#[proc_macro_attribute]
784pub fn tool(_arguments: TokenStream, item: TokenStream) -> TokenStream {
785    let function = parse_macro_input!(item as ItemFn);
786    syn::Error::new_spanned(
787        function.sig.ident,
788        "place `#[post(...)]` or `#[operation(...)]` above `#[mcp::tool(...)]`",
789    )
790    .into_compile_error()
791    .into()
792}
793
794/// Requires a registered security scheme, and optionally scopes, for an
795/// operation.
796///
797/// ```ignore
798/// #[get("/users/{id}", id = "users.read")]
799/// #[security("oauth", scopes = ["users:read"])]
800/// async fn read_user(Path(id): Path<u64>) -> Json<UserView> { .. }
801/// ```
802///
803/// The scheme name is positional and first, and must match a scheme registered
804/// on the application; `scopes` is the only supported option. The requirement
805/// reaches the OpenAPI security block, the contract, and compatibility reports.
806///
807/// Enforcement fails closed. If no registered layer can verify the named
808/// scheme, the request is rejected rather than served unauthenticated, and the
809/// check runs before the body is parsed — in `TestApp`, in native HTTP/1, and
810/// in HTTP/2 alike.
811///
812/// This attribute only annotates; it never expands on its own. It must sit
813/// *below* the operation attribute that owns the function, and says so with a
814/// compile error if it does not.
815#[proc_macro_attribute]
816pub fn security(_arguments: TokenStream, item: TokenStream) -> TokenStream {
817    let function = parse_macro_input!(item as ItemFn);
818    syn::Error::new_spanned(
819        function.sig.ident,
820        "place an HTTP method macro or `#[operation(...)]` above `#[security(...)]`",
821    )
822    .into_compile_error()
823    .into()
824}
825
826fn expand_operation(
827    arguments: TokenStream,
828    item: TokenStream,
829    method: &proc_macro2::TokenStream,
830) -> TokenStream {
831    let arguments = parse_macro_input!(arguments as OperationArgs);
832    let mut function = parse_macro_input!(item as ItemFn);
833
834    match operation_tokens(arguments, &mut function, method) {
835        Ok(tokens) => tokens.into(),
836        Err(error) => error.into_compile_error().into(),
837    }
838}
839
840fn http_method_tokens(method: &HttpMethodArgument) -> syn::Result<proc_macro2::TokenStream> {
841    let method = match method.value.to_ascii_uppercase().as_str() {
842        "GET" => quote!(::blazingly::HttpMethod::Get),
843        "HEAD" => quote!(::blazingly::HttpMethod::Head),
844        "POST" => quote!(::blazingly::HttpMethod::Post),
845        "PUT" => quote!(::blazingly::HttpMethod::Put),
846        "PATCH" => quote!(::blazingly::HttpMethod::Patch),
847        "DELETE" => quote!(::blazingly::HttpMethod::Delete),
848        "OPTIONS" => quote!(::blazingly::HttpMethod::Options),
849        "TRACE" => quote!(::blazingly::HttpMethod::Trace),
850        "CONNECT" => quote!(::blazingly::HttpMethod::Connect),
851        _ => {
852            return Err(syn::Error::new(
853                method.span,
854                "unsupported HTTP method; expected GET, HEAD, POST, PUT, PATCH, \
855                 DELETE, OPTIONS, TRACE, or CONNECT",
856            ));
857        }
858    };
859    Ok(method)
860}
861
862#[allow(clippy::too_many_lines)]
863fn provider_tokens(
864    arguments: &ProviderArgs,
865    function: &ItemFn,
866) -> syn::Result<proc_macro2::TokenStream> {
867    if function.sig.constness.is_some()
868        || matches!(&function.sig.safety, syn::Safety::Unsafe(_))
869        || function.sig.abi.is_some()
870        || function.sig.variadic.is_some()
871        || !function.sig.generics.params.is_empty()
872    {
873        return Err(syn::Error::new_spanned(
874            &function.sig,
875            "Blazingly providers must be plain, non-generic Rust functions",
876        ));
877    }
878    if function.sig.inputs.len() > 8 {
879        return Err(syn::Error::new_spanned(
880            &function.sig.inputs,
881            "Blazingly providers accept at most eight arguments",
882        ));
883    }
884    let mut provider_arguments = Vec::with_capacity(function.sig.inputs.len());
885    for input in &function.sig.inputs {
886        let FnArg::Typed(argument) = input else {
887            return Err(syn::Error::new_spanned(
888                input,
889                "provider arguments must be `Depends<T>` or a typed request \
890                 input: `Path<T>`, `Query<T>`, `Header<T>`, or `Cookie<T>`",
891            ));
892        };
893        let name = operation_argument_name(&argument.pat)?;
894        let Some((kind, inner)) = OperationInputKind::from_type(&argument.ty) else {
895            return Err(syn::Error::new_spanned(
896                &argument.ty,
897                "provider arguments must be `Depends<T>` or a typed request \
898                 input: `Path<T>`, `Query<T>`, `Header<T>`, or `Cookie<T>`",
899            ));
900        };
901        match kind {
902            OperationInputKind::Dependency
903            | OperationInputKind::Path
904            | OperationInputKind::Query
905            | OperationInputKind::Header
906            | OperationInputKind::Cookie => {}
907            _ => {
908                return Err(syn::Error::new_spanned(
909                    &argument.ty,
910                    "a provider reads scalar request inputs; body payloads \
911                     belong to the handler that owns the request body",
912                ));
913            }
914        }
915        let required = wrapper_inner(&inner, "Option").is_none();
916        if matches!(kind, OperationInputKind::Path) && !required {
917            return Err(syn::Error::new_spanned(
918                &argument.ty,
919                "Path<T> arguments are always required and cannot wrap Option<T>",
920            ));
921        }
922        provider_arguments.push((
923            LitStr::new(&name.to_string(), name.span()),
924            kind,
925            (*argument.ty).clone(),
926            inner,
927            required,
928        ));
929    }
930    let request_input_count = provider_arguments
931        .iter()
932        .filter(|(_, kind, ..)| !kind.is_dependency())
933        .count();
934    if request_input_count > 0 && matches!(arguments.lifetime, ProviderLifetimeArgument::Singleton)
935    {
936        return Err(syn::Error::new_spanned(
937            &function.sig,
938            "a singleton provider cannot declare request inputs; it is built \
939             once at compile time, before any request exists",
940        ));
941    }
942
943    let ReturnType::Type(_, output) = &function.sig.output else {
944        return Err(syn::Error::new_spanned(
945            &function.sig,
946            "providers require an explicit output type",
947        ));
948    };
949    let fallible = if let Some((_, error)) = result_types(output) {
950        if !type_is(&error, "DependencyError") {
951            return Err(syn::Error::new_spanned(
952                error,
953                "fallible providers must return `Result<T, DependencyError>`",
954            ));
955        }
956        true
957    } else {
958        false
959    };
960    let asynchronous = function.sig.asyncness.is_some();
961    if asynchronous && matches!(arguments.lifetime, ProviderLifetimeArgument::Singleton) {
962        return Err(syn::Error::new_spanned(
963            &function.sig,
964            "async singleton providers are unsupported because singleton \
965             initialization is deterministic and synchronous at build time",
966        ));
967    }
968
969    let function_name = &function.sig.ident;
970    let provider_module = format_ident!("{function_name}");
971    let visibility = &function.vis;
972
973    if request_input_count > 0 {
974        let body = request_provider_body(
975            arguments,
976            function_name,
977            output,
978            &provider_arguments,
979            asynchronous,
980            fallible,
981        );
982        return Ok(quote! {
983            #function
984
985            #[doc(hidden)]
986            #visibility mod #provider_module {
987                #[allow(unused_imports)]
988                use super::*;
989
990                #[must_use]
991                pub fn provider() -> ::blazingly::RequestProvider {
992                    #body
993                }
994            }
995        });
996    }
997
998    let constructor = match (arguments.lifetime, asynchronous, fallible) {
999        (ProviderLifetimeArgument::Singleton, false, false) => format_ident!("singleton"),
1000        (ProviderLifetimeArgument::Singleton, false, true) => format_ident!("try_singleton"),
1001        (ProviderLifetimeArgument::Request, false, false) => format_ident!("request"),
1002        (ProviderLifetimeArgument::Request, false, true) => format_ident!("try_request"),
1003        (ProviderLifetimeArgument::Transient, false, false) => format_ident!("transient"),
1004        (ProviderLifetimeArgument::Transient, false, true) => format_ident!("try_transient"),
1005        (ProviderLifetimeArgument::Request, true, false) => format_ident!("request_async"),
1006        (ProviderLifetimeArgument::Request, true, true) => format_ident!("try_request_async"),
1007        (ProviderLifetimeArgument::Transient, true, false) => format_ident!("transient_async"),
1008        (ProviderLifetimeArgument::Transient, true, true) => {
1009            format_ident!("try_transient_async")
1010        }
1011        (ProviderLifetimeArgument::Singleton, true, _) => unreachable!(),
1012    };
1013
1014    Ok(quote! {
1015        #function
1016
1017        #[doc(hidden)]
1018        #visibility mod #provider_module {
1019            #[allow(unused_imports)]
1020            use super::*;
1021
1022            #[must_use]
1023            pub fn provider() -> ::blazingly::Provider {
1024                ::blazingly::Provider::#constructor(super::#function_name)
1025            }
1026        }
1027    })
1028}
1029
1030/// Emits the `RequestProvider` for a provider that reads request inputs.
1031///
1032/// Dependencies keep their compiled numeric slots; each request input is
1033/// decoded once per operation by the generated closure below and re-read from
1034/// its slot here, so the provider body is plain calls over typed values with
1035/// no per-request lookup of any kind.
1036#[allow(clippy::too_many_lines)]
1037fn request_provider_body(
1038    arguments: &ProviderArgs,
1039    function_name: &Ident,
1040    output: &Type,
1041    provider_arguments: &[(LitStr, OperationInputKind, Type, Type, bool)],
1042    asynchronous: bool,
1043    fallible: bool,
1044) -> proc_macro2::TokenStream {
1045    let value_type = if fallible {
1046        result_types(output).map_or_else(|| output.clone(), |(success, _)| success)
1047    } else {
1048        output.clone()
1049    };
1050
1051    let mut dependency_keys = Vec::new();
1052    let mut bindings = Vec::new();
1053    let mut call_arguments = Vec::new();
1054    let mut input_records = Vec::new();
1055    let mut depends_index = 0_usize;
1056    let mut input_index = 0_usize;
1057    for (position, (name, kind, wrapper, inner, required)) in provider_arguments.iter().enumerate()
1058    {
1059        let binding = format_ident!("__blazingly_argument_{position}");
1060        if kind.is_dependency() {
1061            let index = depends_index;
1062            depends_index += 1;
1063            dependency_keys.push(quote!(::blazingly::DependencyKey::of::<#inner>()));
1064            bindings.push(quote! {
1065                let #binding = reader.depends::<#inner>(#index)?;
1066            });
1067        } else {
1068            let index = input_index;
1069            input_index += 1;
1070            let source = kind
1071                .source_tokens()
1072                .expect("request provider inputs always have a source");
1073            bindings.push(quote! {
1074                let #binding = (*reader.input::<#wrapper>(#index)?).clone();
1075            });
1076            input_records.push(quote! {
1077                ::blazingly::ProviderInput::new::<#wrapper>(
1078                    ::blazingly::InputDescriptor::new(
1079                        #name,
1080                        #source,
1081                        #required,
1082                        <#inner as ::blazingly::ApiSchema>::type_descriptor(),
1083                    ),
1084                    ::std::rc::Rc::new(|invocation: &::blazingly::InvocationInput<'_>| {
1085                        <#wrapper as ::blazingly::FromInvocation>::from_invocation(
1086                            invocation, #name, #required,
1087                        )
1088                        .map(|value| {
1089                            ::std::rc::Rc::new(value) as ::blazingly::DependencyValue
1090                        })
1091                    }),
1092                ),
1093            });
1094        }
1095        call_arguments.push(binding);
1096    }
1097
1098    let invocation = if fallible {
1099        quote!(super::#function_name(#(#call_arguments),*))
1100    } else {
1101        quote!(::core::result::Result::Ok(super::#function_name(#(#call_arguments),*)))
1102    };
1103    let (constructor, factory) = if asynchronous {
1104        let invocation = if fallible {
1105            quote!(super::#function_name(#(#call_arguments),*).await)
1106        } else {
1107            quote!(::core::result::Result::Ok(
1108                super::#function_name(#(#call_arguments),*).await
1109            ))
1110        };
1111        let constructor = match arguments.lifetime {
1112            ProviderLifetimeArgument::Transient => format_ident!("transient_from_slots_async"),
1113            _ => format_ident!("request_from_slots_async"),
1114        };
1115        let factory = quote! {
1116            ::std::rc::Rc::new(|reader: &::blazingly::SlotReader<'_>| {
1117                let assemble = move || -> ::core::result::Result<_, ::blazingly::DependencyError> {
1118                    #(#bindings)*
1119                    ::core::result::Result::Ok((#(#call_arguments,)*))
1120                };
1121                match assemble() {
1122                    ::core::result::Result::Ok((#(#call_arguments,)*)) => {
1123                        ::std::boxed::Box::pin(async move { #invocation })
1124                    }
1125                    ::core::result::Result::Err(error) => ::std::boxed::Box::pin(async move {
1126                        ::core::result::Result::Err(error)
1127                    }),
1128                }
1129            })
1130        };
1131        (constructor, factory)
1132    } else {
1133        let constructor = match arguments.lifetime {
1134            ProviderLifetimeArgument::Transient => format_ident!("transient_from_slots"),
1135            _ => format_ident!("request_from_slots"),
1136        };
1137        let factory = quote! {
1138            ::std::rc::Rc::new(|reader: &::blazingly::SlotReader<'_>| {
1139                #(#bindings)*
1140                #invocation
1141            })
1142        };
1143        (constructor, factory)
1144    };
1145
1146    quote! {
1147        ::blazingly::RequestProvider::new(
1148            ::blazingly::Provider::#constructor::<#value_type>(
1149                ::std::vec![#(#dependency_keys),*],
1150                #input_index,
1151                #factory,
1152            ),
1153            ::std::vec![#(#input_records)*],
1154        )
1155    }
1156}
1157
1158#[allow(clippy::too_many_lines)]
1159fn operation_tokens(
1160    arguments: OperationArgs,
1161    function: &mut ItemFn,
1162    method: &proc_macro2::TokenStream,
1163) -> syn::Result<proc_macro2::TokenStream> {
1164    let asynchronous = function.sig.asyncness.is_some();
1165    let mcp = take_mcp_arguments(&mut function.attrs)?;
1166    let security = take_security_arguments(&mut function.attrs)?;
1167    let inputs = operation_inputs(&function.sig.inputs)?;
1168    if mcp.is_some()
1169        && let Some(stream) = inputs
1170            .iter()
1171            .find(|input| matches!(input.kind, OperationInputKind::Stream))
1172    {
1173        return Err(syn::Error::new_spanned(
1174            &stream.argument_type,
1175            "streaming request bodies are HTTP-only and cannot be exposed as an MCP tool",
1176        ));
1177    }
1178    let output = operation_output(&function.sig.output)?;
1179    let function_name = &function.sig.ident;
1180    let descriptor_module = format_ident!("{function_name}");
1181    let visibility = &function.vis;
1182    let path = arguments.path;
1183    let id = arguments.id;
1184    let summary = arguments.summary;
1185
1186    let input_descriptors = inputs.iter().filter_map(|input| {
1187        let source = input.kind.source_tokens()?;
1188        let name = &input.name;
1189        let required = input.required;
1190        let inner = &input.inner;
1191        Some(quote! {
1192            ::blazingly::InputDescriptor::new(
1193                #name,
1194                #source,
1195                #required,
1196                <#inner as ::blazingly::ApiSchema>::type_descriptor(),
1197            )
1198        })
1199    });
1200    let dependency_descriptors = inputs
1201        .iter()
1202        .filter(|input| input.kind.is_dependency())
1203        .map(|input| {
1204            let inner = &input.inner;
1205            quote! {
1206                ::blazingly::DependencyDescriptor::new(
1207                    ::core::any::type_name::<#inner>()
1208                )
1209            }
1210        });
1211    let mcp_projection = mcp_projection(mcp, function_name, &summary)?;
1212    let security_requirements = security.iter().map(|security| {
1213        let scheme = &security.scheme;
1214        let scopes = &security.scopes;
1215        quote! {
1216            ::blazingly::SecurityRequirement::new(#scheme)
1217                .with_scopes(::std::vec![#(#scopes.to_owned()),*])
1218        }
1219    });
1220    let status = output.status;
1221    let success_descriptor = output.success.as_ref().map_or_else(
1222        || quote!(::core::option::Option::None),
1223        |success| {
1224            // A borrowed response is written `Json<PageView<'_>>`, and an
1225            // elided lifetime has nothing to be inferred from in the
1226            // descriptor's body. The schema is the same at every lifetime, so
1227            // it is asked for at `'static`.
1228            let success = documented_type(success);
1229            quote!(
1230                ::core::option::Option::Some(
1231                    <#success as ::blazingly::ApiSchema>::type_descriptor()
1232                )
1233            )
1234        },
1235    );
1236    let error_responses = output.error.map_or_else(
1237        || quote!(),
1238        |error| {
1239            quote! {
1240                responses.extend(
1241                    <#error as ::blazingly::ApiError>::response_descriptors()
1242                );
1243            }
1244        },
1245    );
1246    let executable = operation_executable(&inputs, function_name, asynchronous);
1247
1248    Ok(quote! {
1249        #function
1250
1251        #[doc(hidden)]
1252        #visibility mod #descriptor_module {
1253            #[allow(unused_imports)]
1254            use super::*;
1255
1256            #[must_use]
1257            pub fn descriptor() -> ::blazingly::OperationDescriptor {
1258                let mut responses = ::std::vec![
1259                    ::blazingly::ResponseDescriptor::success(
1260                        #status,
1261                        #success_descriptor,
1262                    )
1263                ];
1264                #error_responses
1265                let descriptor = ::blazingly::OperationDescriptor::new(
1266                    #method,
1267                    #path,
1268                    #id,
1269                    #summary,
1270                    ::core::option::Option::None,
1271                    responses,
1272                )
1273                .expect("the operation id was validated by the Blazingly macro")
1274                .with_inputs(::std::vec![#(#input_descriptors),*])
1275                .with_dependencies(::std::vec![#(#dependency_descriptors),*])
1276                .with_security(::std::vec![#(#security_requirements),*]);
1277                #mcp_projection
1278            }
1279
1280            #[must_use]
1281            pub fn executable() -> ::blazingly::ExecutableOperation {
1282                #executable
1283            }
1284        }
1285    })
1286}
1287
1288/// Everything both handler shapes need: the argument prologue, the compiled
1289/// dependency requests, and the call itself.
1290struct ExecutableParts {
1291    extracted_arguments: Vec<proc_macro2::TokenStream>,
1292    dependency_requests: Vec<proc_macro2::TokenStream>,
1293    call: proc_macro2::TokenStream,
1294    input_binding: proc_macro2::TokenStream,
1295    dependency_binding: proc_macro2::TokenStream,
1296}
1297
1298fn operation_executable(
1299    inputs: &[OperationInput],
1300    function_name: &Ident,
1301    asynchronous: bool,
1302) -> proc_macro2::TokenStream {
1303    let parts = executable_parts(inputs, function_name);
1304    if asynchronous {
1305        asynchronous_executable(&parts)
1306    } else {
1307        synchronous_executable(&parts)
1308    }
1309}
1310
1311fn executable_parts(inputs: &[OperationInput], function_name: &Ident) -> ExecutableParts {
1312    let mut dependency_index = 0_usize;
1313    let extracted_arguments = inputs
1314        .iter()
1315        .enumerate()
1316        .map(|(index, input)| {
1317            let binding = format_ident!("__blazingly_argument_{index}");
1318            if input.kind.is_dependency() {
1319                let inner = &input.inner;
1320                let index = dependency_index;
1321                dependency_index += 1;
1322                // A dependency taken by reference is read through the handle
1323                // rather than out of it, so `&Depends<T>` and `&T` share one
1324                // extraction and neither clones `T`.
1325                if input.by_reference || matches!(input.kind, OperationInputKind::Dependency) {
1326                    quote! {
1327                        let #binding = dependencies
1328                            .get::<#inner>(#index)
1329                            .map_err(::blazingly::dependency_error_outcome)?;
1330                    }
1331                } else {
1332                    quote! {
1333                        let #binding = dependencies
1334                            .get_cloned::<#inner>(#index)
1335                            .map_err(::blazingly::dependency_error_outcome)?;
1336                    }
1337                }
1338            } else {
1339                let argument_type = &input.argument_type;
1340                let name = &input.name;
1341                let required = input.required;
1342                quote! {
1343                    let #binding = <#argument_type as ::blazingly::FromInvocation>::from_invocation(
1344                        &input,
1345                        #name,
1346                        #required,
1347                    )
1348                    .map_err(::blazingly::InputRejection::into_execution_outcome)?;
1349                }
1350            }
1351        })
1352        .collect::<Vec<_>>();
1353    let dependency_requests = inputs
1354        .iter()
1355        .filter(|input| input.kind.is_dependency())
1356        .map(|input| {
1357            let inner = &input.inner;
1358            quote!(::blazingly::DependencyRequest::of::<#inner>())
1359        })
1360        .collect::<Vec<_>>();
1361    let handler_arguments = inputs
1362        .iter()
1363        .enumerate()
1364        .map(|(index, input)| {
1365            let binding = format_ident!("__blazingly_argument_{index}");
1366            if input.by_reference {
1367                // `&Depends<T>` is passed straight through; `&T` reaches the
1368                // same place through the handle's `Deref`.
1369                quote!(&#binding)
1370            } else {
1371                quote!(#binding)
1372            }
1373        })
1374        .collect::<Vec<_>>();
1375    let call = quote!(super::#function_name(#(#handler_arguments),*));
1376    // Naming a closure parameter that nothing reads is a warning, and CI
1377    // rejects warnings.
1378    let input_binding = if inputs.iter().any(|input| !input.kind.is_dependency()) {
1379        quote!(input)
1380    } else {
1381        quote!(_)
1382    };
1383    let dependency_binding = if dependency_requests.is_empty() {
1384        quote!(_)
1385    } else {
1386        quote!(dependencies)
1387    };
1388
1389    ExecutableParts {
1390        extracted_arguments,
1391        dependency_requests,
1392        call,
1393        input_binding,
1394        dependency_binding,
1395    }
1396}
1397
1398fn asynchronous_executable(parts: &ExecutableParts) -> proc_macro2::TokenStream {
1399    let ExecutableParts {
1400        extracted_arguments,
1401        dependency_requests,
1402        call,
1403        input_binding,
1404        dependency_binding,
1405    } = parts;
1406    quote! {
1407        ::blazingly::ExecutableOperation::typed_with_dependencies(
1408            descriptor(),
1409            ::std::vec![#(#dependency_requests),*],
1410            |#input_binding, #dependency_binding| {
1411                #(#extracted_arguments)*
1412                ::core::result::Result::Ok(
1413                    ::std::boxed::Box::pin(async move {
1414                        let output = #call.await;
1415                        ::blazingly::OperationOutput::into_execution_outcome(output)
1416                    }) as ::blazingly::OperationFuture
1417                )
1418            },
1419        )
1420    }
1421}
1422
1423/// A handler that is not `async` completes when it is called, so its outcome is
1424/// produced without a future.
1425///
1426/// The fallback exists because `ExecutableOperation` still needs one when
1427/// plugin hooks, cancellation, or request-scoped finalizers wrap the operation.
1428/// It runs the same body at the same point in the pipeline an async handler
1429/// would, so the two paths cannot observe different hook ordering.
1430fn synchronous_executable(parts: &ExecutableParts) -> proc_macro2::TokenStream {
1431    let ExecutableParts {
1432        extracted_arguments,
1433        dependency_requests,
1434        call,
1435        input_binding,
1436        dependency_binding,
1437    } = parts;
1438    quote! {
1439        ::blazingly::ExecutableOperation::typed_sync_with_dependencies(
1440            descriptor(),
1441            ::std::vec![#(#dependency_requests),*],
1442            |#input_binding, #dependency_binding| {
1443                #(#extracted_arguments)*
1444                ::core::result::Result::Ok(
1445                    ::blazingly::OperationOutput::into_execution_outcome(#call)
1446                )
1447            },
1448            |#input_binding, #dependency_binding| {
1449                #(#extracted_arguments)*
1450                ::core::result::Result::Ok(
1451                    ::std::boxed::Box::pin(async move {
1452                        ::blazingly::OperationOutput::into_execution_outcome(#call)
1453                    }) as ::blazingly::OperationFuture
1454                )
1455            },
1456        )
1457    }
1458}
1459
1460fn error_tokens(error: &mut ItemEnum) -> syn::Result<proc_macro2::TokenStream> {
1461    let variants = error
1462        .variants
1463        .iter_mut()
1464        .map(parse_error_variant)
1465        .collect::<syn::Result<Vec<_>>>()?;
1466
1467    let name = &error.ident;
1468    let descriptors = variants.iter().map(error_descriptor_tokens);
1469    let failures = variants.iter().map(error_failure_tokens);
1470
1471    Ok(quote! {
1472        #error
1473
1474        impl ::blazingly::ApiError for #name {
1475            fn response_descriptors() -> ::std::vec::Vec<::blazingly::ResponseDescriptor> {
1476                ::std::vec![#(#descriptors),*]
1477            }
1478
1479            fn into_failure(
1480                self,
1481            ) -> ::core::result::Result<
1482                ::blazingly::OperationFailure,
1483                ::blazingly::ResponseBuildError,
1484            > {
1485                match self {
1486                    #(#failures),*
1487                }
1488            }
1489        }
1490    })
1491}
1492
1493fn parse_error_variant(variant: &mut syn::Variant) -> syn::Result<ErrorVariant> {
1494    let payload = match &variant.fields {
1495        Fields::Unit => None,
1496        Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1497            fields.unnamed.first().map(|field| field.ty.clone())
1498        }
1499        Fields::Unnamed(_) | Fields::Named(_) => {
1500            return Err(syn::Error::new_spanned(
1501                &variant.fields,
1502                "typed errors support unit variants or one unnamed payload",
1503            ));
1504        }
1505    };
1506    let mut status = None;
1507    let mut code = None;
1508    let mut message = None;
1509    let mut headers = Vec::new();
1510    let mut retained = Vec::new();
1511
1512    for attribute in variant.attrs.drain(..) {
1513        if attribute.path().is_ident("status") {
1514            status = Some(attribute.parse_args::<LitInt>()?);
1515        } else if attribute.path().is_ident("code") {
1516            code = Some(attribute.parse_args::<LitStr>()?);
1517        } else if attribute.path().is_ident("message") {
1518            message = Some(attribute.parse_args::<LitStr>()?);
1519        } else if attribute.path().is_ident("header") {
1520            headers.push(parse_error_header(attribute)?);
1521        } else {
1522            retained.push(attribute);
1523        }
1524    }
1525    variant.attrs = retained;
1526    let status = status.ok_or_else(|| {
1527        syn::Error::new_spanned(&variant.ident, "typed errors require `#[status(...)]`")
1528    })?;
1529    let status = status.base10_parse::<u16>()?;
1530    if !(400..=599).contains(&status) {
1531        return Err(syn::Error::new_spanned(
1532            &variant.ident,
1533            "typed error status must be between 400 and 599",
1534        ));
1535    }
1536    let code = code.ok_or_else(|| {
1537        syn::Error::new_spanned(&variant.ident, "typed errors require `#[code(\"...\")]`")
1538    })?;
1539    let message = message.unwrap_or_else(|| LitStr::new(&code.value(), code.span()));
1540    Ok(ErrorVariant {
1541        status,
1542        code,
1543        message,
1544        identifier: variant.ident.clone(),
1545        payload,
1546        headers,
1547    })
1548}
1549
1550fn parse_error_header(attribute: Attribute) -> syn::Result<(LitStr, LitStr)> {
1551    let values = attribute
1552        .parse_args_with(syn::punctuated::Punctuated::<LitStr, Token![,]>::parse_terminated)?;
1553    if values.len() != 2 {
1554        return Err(syn::Error::new_spanned(
1555            attribute,
1556            "response headers require `#[header(\"name\", \"value\")]`",
1557        ));
1558    }
1559    let mut values = values.into_iter();
1560    let name = values
1561        .next()
1562        .ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), "missing header name"))?;
1563    let value = values
1564        .next()
1565        .ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), "missing header value"))?;
1566    validate_response_header(&name, &value)?;
1567    Ok((name, value))
1568}
1569
1570fn error_descriptor_tokens(variant: &ErrorVariant) -> proc_macro2::TokenStream {
1571    let status = variant.status;
1572    let code = &variant.code;
1573    let message = &variant.message;
1574    let body = variant.payload.as_ref().map_or_else(
1575        || quote!(::core::option::Option::None),
1576        |payload| {
1577            quote!(
1578                ::core::option::Option::Some(
1579                    <#payload as ::blazingly::ApiSchema>::type_descriptor()
1580                )
1581            )
1582        },
1583    );
1584    let headers = variant
1585        .headers
1586        .iter()
1587        .map(|(name, value)| quote!(::blazingly::ResponseHeader::new(#name, #value)));
1588    quote!(
1589        ::blazingly::ResponseDescriptor::error(#status, #code, #message, #body)
1590            .with_headers(::std::vec![#(#headers),*])
1591    )
1592}
1593
1594fn error_failure_tokens(variant: &ErrorVariant) -> proc_macro2::TokenStream {
1595    let identifier = &variant.identifier;
1596    let status = variant.status;
1597    let code = &variant.code;
1598    let message = &variant.message;
1599    let pattern = variant.payload.as_ref().map_or_else(
1600        || quote!(Self::#identifier),
1601        |_| quote!(Self::#identifier(payload)),
1602    );
1603    let serialize_payload = variant.payload.as_ref().map_or_else(
1604        || quote!(),
1605        |_| {
1606            quote! {
1607                let details = ::blazingly::__private::blazingly_json::to_vec(&payload)
1608                    .map_err(|_| ::blazingly::ResponseBuildError::serialization_failed())?;
1609                failure = failure.with_details(details);
1610            }
1611        },
1612    );
1613    let apply_headers = variant.headers.iter().map(|(name, value)| {
1614        quote! {
1615            failure = failure.with_header(#name, #value);
1616        }
1617    });
1618    quote! {
1619        #pattern => {
1620            let mut failure = ::blazingly::OperationFailure::new(#status, #code, #message);
1621            #serialize_payload
1622            #(#apply_headers)*
1623            ::core::result::Result::Ok(failure)
1624        }
1625    }
1626}
1627
1628fn validate_response_header(name: &LitStr, value: &LitStr) -> syn::Result<()> {
1629    let valid_name = !name.value().is_empty()
1630        && name.value().bytes().all(|byte| {
1631            byte.is_ascii_alphanumeric()
1632                || matches!(
1633                    byte,
1634                    b'!' | b'#'
1635                        | b'$'
1636                        | b'%'
1637                        | b'&'
1638                        | b'\''
1639                        | b'*'
1640                        | b'+'
1641                        | b'-'
1642                        | b'.'
1643                        | b'^'
1644                        | b'_'
1645                        | b'`'
1646                        | b'|'
1647                        | b'~'
1648                )
1649        });
1650    if !valid_name {
1651        return Err(syn::Error::new(
1652            name.span(),
1653            "response header name contains invalid bytes",
1654        ));
1655    }
1656    if !value
1657        .value()
1658        .bytes()
1659        .all(|byte| byte == b'\t' || (byte >= b' ' && byte != 127))
1660    {
1661        return Err(syn::Error::new(
1662            value.span(),
1663            "response header value contains control bytes",
1664        ));
1665    }
1666    Ok(())
1667}
1668
1669#[allow(clippy::too_many_lines)]
1670fn api_model_tokens(
1671    arguments: &ModelArgs,
1672    item: &mut syn::Item,
1673) -> syn::Result<proc_macro2::TokenStream> {
1674    match item {
1675        syn::Item::Struct(model) => model_tokens(arguments, model),
1676        syn::Item::Enum(model) => enum_model_tokens(arguments, model),
1677        other => Err(syn::Error::new_spanned(
1678            other,
1679            "`#[api_model]` describes a struct, a one-field value type, or a \
1680             unit-variant enum",
1681        )),
1682    }
1683}
1684
1685fn model_tokens(
1686    arguments: &ModelArgs,
1687    model: &mut ItemStruct,
1688) -> syn::Result<proc_macro2::TokenStream> {
1689    if matches!(model.fields, Fields::Unnamed(_)) {
1690        return value_model_tokens(arguments, model);
1691    }
1692    if arguments.borrowed.is_some() {
1693        return borrowed_model_tokens(arguments, model);
1694    }
1695    owned_model_tokens(arguments, model)
1696}
1697
1698/// Expands the value form: one named bundle of field rules, applied by type.
1699///
1700/// `Title` is declared once with the rules written exactly as they are on a
1701/// field, and every model that declares a `Title` field inherits them — into the
1702/// descriptor through [`ApiConstrained::constraint_rules`], and into validation
1703/// through [`ApiConstrained::validate_constraints`]. A newtype was chosen over a
1704/// rule alias because a proc macro cannot see a declaration from another
1705/// expansion: only the type system carries a name across item boundaries.
1706fn value_model_tokens(
1707    arguments: &ModelArgs,
1708    model: &mut ItemStruct,
1709) -> syn::Result<proc_macro2::TokenStream> {
1710    let inner = value_type_inner(arguments, model)?;
1711    let mut rules = take_field_rules(&mut model.attrs)?;
1712    reject_field_only_rules(&rules, &model.ident)?;
1713
1714    let shape = field_shape(&inner);
1715    reject_incompatible_rules(&rules, &inner, shape)?;
1716    normalize_collection_rules(&mut rules, shape);
1717
1718    let model_name = &model.ident;
1719    // A value type has no field name of its own: the model that declares the
1720    // field supplies one when it merges these violations.
1721    let anonymous = LitStr::new("", model_name.span());
1722    let declared_rules = rule_descriptors(&rules, false);
1723    let checks = field_checks(&rules, shape, &anonymous);
1724    let custom = rules.validator.as_ref().map(|validator| {
1725        quote! {
1726            if let ::core::result::Result::Err(custom_errors) = #validator(value) {
1727                ::blazingly::merge_field_validation_errors(
1728                    &mut errors,
1729                    #anonymous,
1730                    &custom_errors,
1731                );
1732            }
1733        }
1734    });
1735
1736    Ok(quote! {
1737        #[derive(
1738            ::blazingly::__private::serde::Serialize,
1739            ::blazingly::__private::serde::Deserialize
1740        )]
1741        #[serde(crate = "::blazingly::__private::serde")]
1742        #[serde(transparent)]
1743        #model
1744
1745        impl #model_name {
1746            /// Wraps a value without checking it; the declared rules run when a
1747            /// request is validated.
1748            #[must_use]
1749            pub fn new(value: #inner) -> Self {
1750                Self(value)
1751            }
1752
1753            /// The wrapped value.
1754            #[must_use]
1755            pub const fn as_inner(&self) -> &#inner {
1756                &self.0
1757            }
1758
1759            /// Unwraps the value.
1760            #[must_use]
1761            pub fn into_inner(self) -> #inner {
1762                self.0
1763            }
1764        }
1765
1766        const _: () = {
1767            impl ::blazingly::ApiSchema for #model_name {
1768                fn type_descriptor() -> ::blazingly::TypeDescriptor {
1769                    let mut descriptor =
1770                        <#inner as ::blazingly::ApiSchema>::type_descriptor();
1771                    descriptor.rust_name = ::std::string::String::from(
1772                        stringify!(#model_name)
1773                    );
1774                    // Carried on the type, not only inherited by the field that
1775                    // uses it: a `Vec<#model_name>` item has no field name.
1776                    descriptor.constraints.extend(
1777                        <Self as ::blazingly::ApiConstrained>::constraint_rules(),
1778                    );
1779                    descriptor
1780                }
1781
1782                fn validate_input(
1783                    &self,
1784                ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
1785                    <Self as ::blazingly::ApiConstrained>::validate_constraints(self)
1786                }
1787            }
1788
1789            impl ::blazingly::ApiConstrained for #model_name {
1790                fn constraint_rules() -> ::std::vec::Vec<::blazingly::ValidationRule> {
1791                    ::std::vec![#(#declared_rules),*]
1792                }
1793
1794                fn validate_constraints(
1795                    &self,
1796                ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
1797                    let mut errors = ::blazingly::ValidationErrors::new();
1798                    {
1799                        let value = &self.0;
1800                        #checks
1801                        #custom
1802                    }
1803
1804                    if errors.is_empty() {
1805                        ::core::result::Result::Ok(())
1806                    } else {
1807                        ::core::result::Result::Err(errors)
1808                    }
1809                }
1810            }
1811        };
1812    })
1813}
1814
1815/// Resolves the one type a value type wraps, rejecting every other shape.
1816fn value_type_inner(arguments: &ModelArgs, model: &ItemStruct) -> syn::Result<Type> {
1817    let Fields::Unnamed(fields) = &model.fields else {
1818        unreachable!("the value form is selected by an unnamed field list");
1819    };
1820    if fields.unnamed.len() != 1 {
1821        return Err(syn::Error::new_spanned(
1822            &model.fields,
1823            "a value type wraps exactly one field; declare a struct with named \
1824             fields for anything else",
1825        ));
1826    }
1827    if let Some(borrowed) = &arguments.borrowed {
1828        return Err(syn::Error::new_spanned(
1829            borrowed,
1830            "a value type is validated wherever it appears, which a borrowed \
1831             output view never is",
1832        ));
1833    }
1834    if let Some(rename) = &arguments.rename_all {
1835        return Err(syn::Error::new_spanned(
1836            rename,
1837            "`rename_all` renames fields, and a value type has none",
1838        ));
1839    }
1840    if let Some(validator) = &arguments.validator {
1841        return Err(syn::Error::new_spanned(
1842            validator,
1843            "declare `#[validate_with(...)]` beside the other rules; a value type \
1844             has no cross-field check to run",
1845        ));
1846    }
1847    reject_owned_generics(&model.generics)?;
1848
1849    let inner = fields.unnamed[0].ty.clone();
1850    if wrapper_inner(&inner, "Option").is_some() {
1851        return Err(syn::Error::new_spanned(
1852            &inner,
1853            "a value type wraps the value its rules describe; declare the field \
1854             that uses it as `Option<_>` instead",
1855        ));
1856    }
1857    Ok(inner)
1858}
1859
1860/// Rejects the rules that only mean something on a field of a model.
1861fn reject_field_only_rules(rules: &FieldRules, model_name: &Ident) -> syn::Result<()> {
1862    if let Some(alias) = rules.aliases.first() {
1863        return Err(syn::Error::new_spanned(
1864            alias,
1865            "`alias` names an extra wire key for one field, which a value type \
1866             does not have",
1867        ));
1868    }
1869    if let Some((_, span)) = &rules.default {
1870        return Err(syn::Error::new(
1871            *span,
1872            "a `default` belongs to the field that may be absent, not to the type \
1873             the field is declared with",
1874        ));
1875    }
1876    if rules.nested {
1877        return Err(syn::Error::new_spanned(
1878            model_name,
1879            "`nested` recurses into a model; a value type validates itself",
1880        ));
1881    }
1882    Ok(())
1883}
1884
1885/// Expands the enumeration form: a string schema with a closed variant set.
1886fn enum_model_tokens(
1887    arguments: &ModelArgs,
1888    model: &mut ItemEnum,
1889) -> syn::Result<proc_macro2::TokenStream> {
1890    if let Some(borrowed) = &arguments.borrowed {
1891        return Err(syn::Error::new_spanned(
1892            borrowed,
1893            "an enumeration owns its variants and never borrows",
1894        ));
1895    }
1896    if let Some(validator) = &arguments.validator {
1897        return Err(syn::Error::new_spanned(
1898            validator,
1899            "an enumeration accepts its declared variants and nothing else, so \
1900             there is nothing left to validate",
1901        ));
1902    }
1903    if let Some(parameter) = model.generics.params.first() {
1904        return Err(syn::Error::new_spanned(
1905            parameter,
1906            "an API enumeration is a closed set of strings and cannot be generic",
1907        ));
1908    }
1909    if model.variants.is_empty() {
1910        return Err(syn::Error::new_spanned(
1911            &model.ident,
1912            "an API enumeration needs at least one variant",
1913        ));
1914    }
1915
1916    let wire_names = enum_wire_names(arguments, model)?;
1917    let identifiers = model
1918        .variants
1919        .iter()
1920        .map(|variant| variant.ident.clone())
1921        .collect::<Vec<_>>();
1922    let model_name = &model.ident;
1923    let encoded = format!(
1924        "enum={}",
1925        wire_names
1926            .iter()
1927            .map(LitStr::value)
1928            .collect::<Vec<_>>()
1929            .join("|")
1930    );
1931
1932    Ok(quote! {
1933        #[derive(
1934            ::blazingly::__private::serde::Serialize,
1935            ::blazingly::__private::serde::Deserialize
1936        )]
1937        #[serde(crate = "::blazingly::__private::serde")]
1938        #model
1939
1940        impl #model_name {
1941            /// Every accepted wire value, in declaration order.
1942            pub const VARIANTS: &'static [&'static str] = &[#(#wire_names),*];
1943
1944            /// The wire value this variant serializes to.
1945            #[must_use]
1946            pub const fn as_str(&self) -> &'static str {
1947                match self {
1948                    #(Self::#identifiers => #wire_names,)*
1949                }
1950            }
1951        }
1952
1953        const _: () = {
1954            impl ::blazingly::ApiSchema for #model_name {
1955                fn type_descriptor() -> ::blazingly::TypeDescriptor {
1956                    ::blazingly::TypeDescriptor::scalar(
1957                        stringify!(#model_name),
1958                        ::blazingly::SchemaKind::String,
1959                    )
1960                    .with_constraints(
1961                        <Self as ::blazingly::ApiConstrained>::constraint_rules(),
1962                    )
1963                }
1964            }
1965
1966            impl ::blazingly::ApiConstrained for #model_name {
1967                fn constraint_rules() -> ::std::vec::Vec<::blazingly::ValidationRule> {
1968                    ::std::vec![::blazingly::ValidationRule::Custom(#encoded.to_owned())]
1969                }
1970
1971                fn validate_constraints(
1972                    &self,
1973                ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
1974                    ::core::result::Result::Ok(())
1975                }
1976            }
1977        };
1978    })
1979}
1980
1981/// Resolves every variant's wire value and pins it with `#[serde(rename)]`.
1982fn enum_wire_names(arguments: &ModelArgs, model: &mut ItemEnum) -> syn::Result<Vec<LitStr>> {
1983    let rename_rule = enum_rename_rule(arguments)?;
1984    let mut wire_names: Vec<LitStr> = Vec::new();
1985
1986    for variant in &mut model.variants {
1987        if !matches!(variant.fields, Fields::Unit) {
1988            return Err(syn::Error::new_spanned(
1989                &variant.fields,
1990                "an API enumeration projects to a string, so a variant cannot \
1991                 carry data",
1992            ));
1993        }
1994        let wire = variant_wire_name(variant, rename_rule)?;
1995        if wire.value().contains('|') {
1996            return Err(syn::Error::new(
1997                wire.span(),
1998                "`|` separates the variants in the recorded schema and cannot \
1999                 appear in one",
2000            ));
2001        }
2002        if wire_names
2003            .iter()
2004            .any(|declared| declared.value() == wire.value())
2005        {
2006            return Err(syn::Error::new(
2007                wire.span(),
2008                format!("the wire value {:?} is declared twice", wire.value()),
2009            ));
2010        }
2011        variant
2012            .attrs
2013            .push(syn::parse_quote!(#[serde(rename = #wire)]));
2014        wire_names.push(wire);
2015    }
2016
2017    Ok(wire_names)
2018}
2019
2020/// Resolves one variant's wire value, consuming an explicit `#[rename("...")]`.
2021fn variant_wire_name(variant: &mut syn::Variant, rule: RenameRule) -> syn::Result<LitStr> {
2022    let mut explicit = None;
2023    let mut retained = Vec::new();
2024    for attribute in variant.attrs.drain(..) {
2025        if attribute.path().is_ident("rename") {
2026            if explicit.is_some() {
2027                return Err(syn::Error::new_spanned(
2028                    attribute,
2029                    "only one `rename` may be declared per variant",
2030                ));
2031            }
2032            explicit = Some(attribute.parse_args::<LitStr>()?);
2033        } else {
2034            retained.push(attribute);
2035        }
2036    }
2037    variant.attrs = retained;
2038
2039    Ok(explicit.unwrap_or_else(|| {
2040        LitStr::new(
2041            &rule.apply(&variant.ident.to_string()),
2042            variant.ident.span(),
2043        )
2044    }))
2045}
2046
2047/// The variant renaming rules an API enumeration understands.
2048///
2049/// Every variant is emitted with an explicit `#[serde(rename = "...")]`, so the
2050/// recorded schema and the wire form cannot drift apart.
2051#[derive(Clone, Copy)]
2052enum RenameRule {
2053    Pascal,
2054    Lower,
2055    Upper,
2056    Camel,
2057    Snake,
2058    ScreamingSnake,
2059    Kebab,
2060    ScreamingKebab,
2061}
2062
2063impl RenameRule {
2064    const SUPPORTED: &'static str = "`PascalCase`, `lowercase`, `UPPERCASE`, `camelCase`, \
2065                                     `snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`, \
2066                                     or `SCREAMING-KEBAB-CASE`";
2067
2068    fn parse(value: &str) -> Option<Self> {
2069        Some(match value {
2070            "PascalCase" => Self::Pascal,
2071            "lowercase" => Self::Lower,
2072            "UPPERCASE" => Self::Upper,
2073            "camelCase" => Self::Camel,
2074            "snake_case" => Self::Snake,
2075            "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
2076            "kebab-case" => Self::Kebab,
2077            "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
2078            _ => return None,
2079        })
2080    }
2081
2082    fn apply(self, variant: &str) -> String {
2083        match self {
2084            Self::Pascal => variant.to_owned(),
2085            Self::Lower => variant.to_ascii_lowercase(),
2086            Self::Upper => variant.to_ascii_uppercase(),
2087            Self::Camel => {
2088                let mut characters = variant.chars();
2089                characters.next().map_or_else(String::new, |first| {
2090                    first.to_ascii_lowercase().to_string() + characters.as_str()
2091                })
2092            }
2093            Self::Snake => snake_case(variant),
2094            Self::ScreamingSnake => snake_case(variant).to_ascii_uppercase(),
2095            Self::Kebab => snake_case(variant).replace('_', "-"),
2096            Self::ScreamingKebab => snake_case(variant).to_ascii_uppercase().replace('_', "-"),
2097        }
2098    }
2099}
2100
2101fn snake_case(variant: &str) -> String {
2102    let mut output = String::with_capacity(variant.len());
2103    for (index, character) in variant.char_indices() {
2104        if index > 0 && character.is_uppercase() {
2105            output.push('_');
2106        }
2107        output.push(character.to_ascii_lowercase());
2108    }
2109    output
2110}
2111
2112fn enum_rename_rule(arguments: &ModelArgs) -> syn::Result<RenameRule> {
2113    let Some(rename) = &arguments.rename_all else {
2114        return Ok(RenameRule::Pascal);
2115    };
2116    RenameRule::parse(&rename.value()).ok_or_else(|| {
2117        syn::Error::new(
2118            rename.span(),
2119            format!(
2120                "an enumeration renames its variants with {}",
2121                RenameRule::SUPPORTED
2122            ),
2123        )
2124    })
2125}
2126
2127/// Expands the owning form: `Serialize`, `Deserialize`, and a validating
2128/// [`ApiModel`] implementation.
2129fn owned_model_tokens(
2130    arguments: &ModelArgs,
2131    model: &mut ItemStruct,
2132) -> syn::Result<proc_macro2::TokenStream> {
2133    let Fields::Named(fields) = &mut model.fields else {
2134        return Err(syn::Error::new_spanned(
2135            &model.fields,
2136            "`#[api_model]` requires a struct with named fields",
2137        ));
2138    };
2139    reject_owned_generics(&model.generics)?;
2140
2141    let rename_rule = model_rename_rule(arguments)?;
2142    let model_name = model.ident.clone();
2143    let OwnedFields {
2144        descriptors,
2145        validations,
2146        defaults,
2147        needs_probes,
2148    } = owned_field_tokens(fields, &rename_rule, &model_name)?;
2149
2150    let serde_rename = arguments
2151        .rename_all
2152        .as_ref()
2153        .map_or_else(|| quote!(), |rename| quote!(#[serde(rename_all = #rename)]));
2154    let model_validation = arguments.validator.as_ref().map(|validator| {
2155        quote! {
2156            if let ::core::result::Result::Err(failure) = #validator(self) {
2157                ::blazingly::validation::merge_model_violations(&mut errors, failure);
2158            }
2159        }
2160    });
2161    let probes = if needs_probes {
2162        nested_probe_definitions()
2163    } else {
2164        quote!()
2165    };
2166
2167    Ok(quote! {
2168        #[derive(
2169            ::blazingly::__private::serde::Serialize,
2170            ::blazingly::__private::serde::Deserialize
2171        )]
2172        #[serde(crate = "::blazingly::__private::serde")]
2173        #serde_rename
2174        #model
2175
2176        #(#defaults)*
2177
2178        const _: () = {
2179            #probes
2180
2181            impl ::blazingly::ApiModel for #model_name {
2182                fn model_descriptor() -> ::blazingly::ModelDescriptor {
2183                    ::blazingly::ModelDescriptor::new(
2184                        stringify!(#model_name),
2185                        ::std::vec![#(#descriptors),*],
2186                    )
2187                }
2188
2189                fn validate(
2190                    &self,
2191                ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
2192                    let mut errors = ::blazingly::ValidationErrors::new();
2193                    #(#validations)*
2194                    #model_validation
2195
2196                    if errors.is_empty() {
2197                        ::core::result::Result::Ok(())
2198                    } else {
2199                        ::core::result::Result::Err(errors)
2200                    }
2201                }
2202            }
2203        };
2204    })
2205}
2206
2207#[derive(Default)]
2208struct OwnedFields {
2209    descriptors: Vec<proc_macro2::TokenStream>,
2210    validations: Vec<proc_macro2::TokenStream>,
2211    /// Serde default functions, emitted beside the model rather than inside it
2212    /// because `#[serde(default = "...")]` names a path in the enclosing scope.
2213    defaults: Vec<proc_macro2::TokenStream>,
2214    needs_probes: bool,
2215}
2216
2217fn owned_field_tokens(
2218    fields: &mut syn::FieldsNamed,
2219    rename_rule: &str,
2220    model_name: &Ident,
2221) -> syn::Result<OwnedFields> {
2222    let mut owned = OwnedFields::default();
2223
2224    for field in &mut fields.named {
2225        let identifier = field
2226            .ident
2227            .clone()
2228            .expect("named fields always have identifiers");
2229        let mut rules = take_field_rules(&mut field.attrs)?;
2230        for alias in &rules.aliases {
2231            field
2232                .attrs
2233                .push(syn::parse_quote!(#[serde(alias = #alias)]));
2234        }
2235        let field_type = field.ty.clone();
2236        let optional = wrapper_inner(&field_type, "Option");
2237        let validation_type = optional.as_ref().unwrap_or(&field_type);
2238        let shape = field_shape(validation_type);
2239        reject_incompatible_rules(&rules, validation_type, shape)?;
2240        normalize_collection_rules(&mut rules, shape);
2241        owned.needs_probes |= shape.may_be_model();
2242
2243        if let Some((literal, span)) = &rules.default {
2244            if optional.is_some() {
2245                return Err(syn::Error::new(
2246                    *span,
2247                    "a field with a `default` is never absent from the handler; \
2248                     declare it without `Option`",
2249                ));
2250            }
2251            let function = default_function_name(model_name, &identifier);
2252            let expression = literal.expression();
2253            let path = LitStr::new(&function.to_string(), *span);
2254            field
2255                .attrs
2256                .push(syn::parse_quote!(#[serde(default = #path)]));
2257            owned.defaults.push(quote! {
2258                #[doc(hidden)]
2259                fn #function() -> #field_type {
2260                    #expression
2261                }
2262            });
2263        }
2264
2265        let public_name = public_field_name(&identifier, rename_rule);
2266        let required = optional.is_none() && rules.default.is_none();
2267        let declared_rules = rule_descriptors(&rules, optional.is_some());
2268        let inherited_rules = inherited_rule_descriptor(validation_type, shape);
2269        let nested_descriptor = nested_rule_descriptor(validation_type, shape, rules.nested);
2270        owned.descriptors.push(quote! {
2271            ::blazingly::FieldDescriptor::new(
2272                #public_name,
2273                #required,
2274                <#field_type as ::blazingly::ApiSchema>::type_descriptor(),
2275                {
2276                    let mut rules = ::std::vec::Vec::new();
2277                    #inherited_rules
2278                    #(rules.push(#declared_rules);)*
2279                    #nested_descriptor
2280                    rules
2281                },
2282            )
2283        });
2284
2285        let checks = field_checks(&rules, shape, &public_name);
2286        let nested_checks = nested_validation_checks(shape, &public_name, &rules);
2287        if checks.is_empty() && nested_checks.is_empty() {
2288            continue;
2289        }
2290        if optional.is_some() {
2291            owned.validations.push(quote! {
2292                if let ::core::option::Option::Some(value) = &self.#identifier {
2293                    #checks
2294                    #nested_checks
2295                }
2296            });
2297        } else {
2298            owned.validations.push(quote! {
2299                {
2300                    let value = &self.#identifier;
2301                    #checks
2302                    #nested_checks
2303                }
2304            });
2305        }
2306    }
2307
2308    Ok(owned)
2309}
2310
2311/// Names the serde default function for one field.
2312///
2313/// The model name is folded in because two models in one module may both
2314/// declare a defaulted field of the same name.
2315fn default_function_name(model_name: &Ident, field: &Ident) -> Ident {
2316    let model = model_name.to_string().to_lowercase();
2317    let field = field.to_string();
2318    let field = field.trim_start_matches("r#");
2319    format_ident!("__blazingly_default_{model}_{field}")
2320}
2321
2322fn public_field_name(identifier: &Ident, rename_rule: &str) -> LitStr {
2323    let name = if rename_rule == "camelCase" {
2324        snake_to_camel(&identifier.to_string())
2325    } else {
2326        identifier.to_string()
2327    };
2328    LitStr::new(&name, identifier.span())
2329}
2330
2331/// Expands the borrowed form: `Serialize` plus a direct [`ApiSchema`] impl.
2332///
2333/// A borrowed view is an output type. It is produced by the operation rather
2334/// than parsed from a client, so it gets neither `Deserialize` nor validation,
2335/// and it may carry lifetime and type parameters that an owning model cannot.
2336fn borrowed_model_tokens(
2337    arguments: &ModelArgs,
2338    model: &mut ItemStruct,
2339) -> syn::Result<proc_macro2::TokenStream> {
2340    let Fields::Named(fields) = &mut model.fields else {
2341        return Err(syn::Error::new_spanned(
2342            &model.fields,
2343            "`#[api_model]` requires a struct with named fields",
2344        ));
2345    };
2346    if let Some(validator) = &arguments.validator {
2347        return Err(syn::Error::new_spanned(
2348            validator,
2349            "a borrowed view is an output type and is never validated; declare \
2350             `validate_with` on the owning model the client sends",
2351        ));
2352    }
2353
2354    let rename_rule = model_rename_rule(arguments)?;
2355    let mut descriptors = Vec::new();
2356
2357    for field in &mut fields.named {
2358        let identifier = field
2359            .ident
2360            .clone()
2361            .expect("named fields always have identifiers");
2362        reject_borrowed_field_rules(&mut field.attrs)?;
2363
2364        let public_name = public_field_name(&identifier, &rename_rule);
2365        let required = wrapper_inner(&field.ty, "Option").is_none();
2366        // The wire shape a borrowed view prints is the shape its owning
2367        // counterpart prints, so the documented schema is the field type with
2368        // its borrows resolved: `&'store str` is a string, `Vec<&'store Tag>`
2369        // is an array of `Tag`.
2370        let schema_type = schema_type(&field.ty);
2371        // A view is never validated, but an optional field still prints `null`,
2372        // and a reader of the document has no other way to learn that.
2373        let metadata = if required {
2374            quote!(::std::vec::Vec::new())
2375        } else {
2376            quote!(::std::vec![::blazingly::ValidationRule::Custom(
2377                "nullable=true".to_owned()
2378            )])
2379        };
2380        descriptors.push(quote! {
2381            ::blazingly::FieldDescriptor::new(
2382                #public_name,
2383                #required,
2384                <#schema_type as ::blazingly::ApiSchema>::type_descriptor(),
2385                #metadata,
2386            )
2387        });
2388    }
2389
2390    let model_name = &model.ident;
2391    let serde_rename = arguments
2392        .rename_all
2393        .as_ref()
2394        .map_or_else(|| quote!(), |rename| quote!(#[serde(rename_all = #rename)]));
2395    let (impl_generics, type_generics, where_clause) = model.generics.split_for_impl();
2396    let schema_bounds = schema_parameter_bounds(&model.generics);
2397    let where_clause = merge_where_predicates(where_clause, &schema_bounds);
2398    let descriptor_name = borrowed_descriptor_name(&model.generics, model_name);
2399
2400    Ok(quote! {
2401        #[derive(::blazingly::__private::serde::Serialize)]
2402        #[serde(crate = "::blazingly::__private::serde")]
2403        #serde_rename
2404        #model
2405
2406        const _: () = {
2407            #[allow(dead_code)]
2408            fn __blazingly_schema_name(
2409                base: &str,
2410                parameters: &[::blazingly::TypeDescriptor],
2411            ) -> ::std::string::String {
2412                let mut name = ::std::string::String::from(base);
2413                for parameter in parameters {
2414                    name.push('_');
2415                    // `OpenAPI` component keys accept only `[A-Za-z0-9._-]`,
2416                    // and `Vec<Tag>` is a perfectly ordinary Rust name.
2417                    for character in parameter.rust_name.chars() {
2418                        if character.is_ascii_alphanumeric()
2419                            || character == '_'
2420                            || character == '.'
2421                            || character == '-'
2422                        {
2423                            name.push(character);
2424                        } else {
2425                            name.push('_');
2426                        }
2427                    }
2428                }
2429                name
2430            }
2431
2432            impl #impl_generics ::blazingly::ApiSchema for #model_name #type_generics
2433            #where_clause
2434            {
2435                fn type_descriptor() -> ::blazingly::TypeDescriptor {
2436                    ::blazingly::TypeDescriptor::model(
2437                        ::blazingly::ModelDescriptor::new(
2438                            #descriptor_name,
2439                            ::std::vec![#(#descriptors),*],
2440                        )
2441                    )
2442                }
2443            }
2444        };
2445    })
2446}
2447
2448/// An owning model is deserialized and validated, and neither survives an
2449/// unbounded parameter, so generics are the borrowed form's alone.
2450fn reject_owned_generics(generics: &syn::Generics) -> syn::Result<()> {
2451    let Some(parameter) = generics.params.first() else {
2452        return Ok(());
2453    };
2454    let reason = match parameter {
2455        syn::GenericParam::Lifetime(_) => {
2456            "an owning model deserializes into itself and cannot borrow from the \
2457             request buffer"
2458        }
2459        syn::GenericParam::Type(_) | syn::GenericParam::Const(_) => {
2460            "field rules cannot recurse into an unbounded parameter, so an owning \
2461             model would silently skip validating it"
2462        }
2463    };
2464    Err(syn::Error::new_spanned(
2465        parameter,
2466        format!("{reason}; declare `#[api_model(borrowed)]` for a generic output view"),
2467    ))
2468}
2469
2470/// Every declarative rule is a request-side check, so none of them mean
2471/// anything on a view the framework only ever writes.
2472fn reject_borrowed_field_rules(attributes: &mut Vec<Attribute>) -> syn::Result<()> {
2473    let mut retained = Vec::new();
2474    for attribute in attributes.drain(..) {
2475        if BORROWED_REJECTED_ATTRIBUTES
2476            .iter()
2477            .any(|name| attribute.path().is_ident(name))
2478        {
2479            let name = attribute
2480                .path()
2481                .get_ident()
2482                .map_or_else(String::new, ToString::to_string);
2483            let purpose = if name == "default" {
2484                "fills in an absent request field"
2485            } else {
2486                "validates a request"
2487            };
2488            return Err(syn::Error::new(
2489                attribute_span(&attribute),
2490                format!(
2491                    "`#[{name}]` {purpose}; a borrowed view is an output type and is \
2492                     never validated. Declare the rule on the owning model the client \
2493                     sends"
2494                ),
2495            ));
2496        }
2497        retained.push(attribute);
2498    }
2499    *attributes = retained;
2500    Ok(())
2501}
2502
2503const BORROWED_REJECTED_ATTRIBUTES: &[&str] = &[
2504    "min_length",
2505    "max_length",
2506    "email",
2507    "alias",
2508    "validate_with",
2509    "nested",
2510    "default",
2511    "minimum",
2512    "maximum",
2513    "exclusive_minimum",
2514    "exclusive_maximum",
2515    "multiple_of",
2516    "pattern",
2517    "min_items",
2518    "max_items",
2519    "unique_items",
2520];
2521
2522/// Adds `T: ApiSchema` for every type parameter so the field descriptors can
2523/// ask each one for its own schema.
2524fn schema_parameter_bounds(generics: &syn::Generics) -> Vec<syn::WherePredicate> {
2525    generics
2526        .params
2527        .iter()
2528        .filter_map(|parameter| match parameter {
2529            syn::GenericParam::Type(parameter) => {
2530                let identifier = &parameter.ident;
2531                Some(syn::parse_quote!(#identifier: ::blazingly::ApiSchema))
2532            }
2533            syn::GenericParam::Lifetime(_) | syn::GenericParam::Const(_) => None,
2534        })
2535        .collect()
2536}
2537
2538fn merge_where_predicates(
2539    existing: Option<&syn::WhereClause>,
2540    added: &[syn::WherePredicate],
2541) -> Option<syn::WhereClause> {
2542    if added.is_empty() {
2543        return existing.cloned();
2544    }
2545    let mut clause = existing.cloned().unwrap_or_else(|| syn::WhereClause {
2546        where_token: <Token![where]>::default(),
2547        predicates: syn::punctuated::Punctuated::new(),
2548    });
2549    clause.predicates.extend(added.iter().cloned());
2550    Some(clause)
2551}
2552
2553/// One `Page<'store, T>` describes every paginated response, so the documented
2554/// name has to distinguish `Page<Article>` from `Page<Company>`; an `OpenAPI`
2555/// projection keys its component schemas by this name.
2556fn borrowed_descriptor_name(
2557    generics: &syn::Generics,
2558    model_name: &Ident,
2559) -> proc_macro2::TokenStream {
2560    let parameters = generics
2561        .params
2562        .iter()
2563        .filter_map(|parameter| match parameter {
2564            syn::GenericParam::Type(parameter) => {
2565                let identifier = &parameter.ident;
2566                Some(quote!(<#identifier as ::blazingly::ApiSchema>::type_descriptor()))
2567            }
2568            syn::GenericParam::Lifetime(_) | syn::GenericParam::Const(_) => None,
2569        })
2570        .collect::<Vec<_>>();
2571    if parameters.is_empty() {
2572        return quote!(stringify!(#model_name));
2573    }
2574    quote! {
2575        __blazingly_schema_name(
2576            stringify!(#model_name),
2577            &[#(#parameters),*],
2578        )
2579    }
2580}
2581
2582/// Resolves a written field type to the type whose schema it prints.
2583///
2584/// A borrowed view exists to avoid owning its data, so its fields are written
2585/// as `&'store str`, `Vec<&'store Tag>`, or `Cow<'store, str>`. All three print
2586/// exactly what their owning counterparts print, and this is where that is
2587/// stated once instead of by every application that writes a view.
2588fn schema_type(ty: &Type) -> Type {
2589    match ty {
2590        Type::Reference(reference) => borrowed_schema_type(&reference.elem),
2591        Type::Slice(slice) => vector_of(&schema_type(&slice.elem)),
2592        Type::Array(array) => vector_of(&schema_type(&array.elem)),
2593        Type::Paren(inner) => schema_type(&inner.elem),
2594        Type::Group(inner) => schema_type(&inner.elem),
2595        Type::Path(path) => path_schema_type(path),
2596        other => other.clone(),
2597    }
2598}
2599
2600fn borrowed_schema_type(referent: &Type) -> Type {
2601    // `&str` is the schema the contract implements; `str` alone is not.
2602    if bare_type_matches(referent, &["str"]) {
2603        return syn::parse_quote!(&str);
2604    }
2605    match referent {
2606        Type::Slice(slice) => vector_of(&schema_type(&slice.elem)),
2607        Type::Array(array) => vector_of(&schema_type(&array.elem)),
2608        other => schema_type(other),
2609    }
2610}
2611
2612fn path_schema_type(path: &TypePath) -> Type {
2613    let mut path = path.clone();
2614    if let Some(segment) = path.path.segments.last()
2615        && segment.ident == "Cow"
2616        && let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments
2617    {
2618        let owned = arguments.args.iter().find_map(|argument| match argument {
2619            syn::GenericArgument::Type(ty) => Some(ty),
2620            _ => None,
2621        });
2622        if let Some(owned) = owned {
2623            return borrowed_schema_type(owned);
2624        }
2625    }
2626    for segment in &mut path.path.segments {
2627        let syn::PathArguments::AngleBracketed(arguments) = &mut segment.arguments else {
2628            continue;
2629        };
2630        let resolved = arguments
2631            .args
2632            .iter()
2633            .filter_map(|argument| match argument {
2634                syn::GenericArgument::Lifetime(_) => None,
2635                syn::GenericArgument::Type(ty) => Some(syn::GenericArgument::Type(schema_type(ty))),
2636                other => Some(other.clone()),
2637            })
2638            .collect::<syn::punctuated::Punctuated<_, Token![,]>>();
2639        if resolved.is_empty() {
2640            segment.arguments = syn::PathArguments::None;
2641        } else {
2642            arguments.args = resolved;
2643        }
2644    }
2645    Type::Path(path)
2646}
2647
2648fn vector_of(item: &Type) -> Type {
2649    syn::parse_quote!(::std::vec::Vec<#item>)
2650}
2651
2652/// Names a type in a descriptor body, where no lifetime can be inferred.
2653///
2654/// `Json<PageView<'_>>` and `Json<PageView<'store>>` document one schema, so
2655/// every lifetime is written `'static` and the impl that answers is the same
2656/// one either way.
2657fn documented_type(ty: &Type) -> Type {
2658    match ty {
2659        Type::Reference(reference) => {
2660            let mut reference = reference.clone();
2661            reference.lifetime = Some(syn::Lifetime::new(
2662                "'static",
2663                proc_macro2::Span::call_site(),
2664            ));
2665            reference.elem = Box::new(documented_type(&reference.elem));
2666            Type::Reference(reference)
2667        }
2668        Type::Path(path) => {
2669            let mut path = path.clone();
2670            for segment in &mut path.path.segments {
2671                let syn::PathArguments::AngleBracketed(arguments) = &mut segment.arguments else {
2672                    continue;
2673                };
2674                for argument in &mut arguments.args {
2675                    match argument {
2676                        syn::GenericArgument::Lifetime(lifetime) => {
2677                            *lifetime =
2678                                syn::Lifetime::new("'static", proc_macro2::Span::call_site());
2679                        }
2680                        syn::GenericArgument::Type(inner) => *inner = documented_type(inner),
2681                        _ => {}
2682                    }
2683                }
2684            }
2685            Type::Path(path)
2686        }
2687        Type::Paren(inner) => documented_type(&inner.elem),
2688        Type::Group(inner) => documented_type(&inner.elem),
2689        Type::Slice(slice) => {
2690            let mut slice = slice.clone();
2691            slice.elem = Box::new(documented_type(&slice.elem));
2692            Type::Slice(slice)
2693        }
2694        other => other.clone(),
2695    }
2696}
2697
2698/// Emits the autoref-specialization probes that drive recursion into models.
2699///
2700/// The specialized trait is implemented for the probe itself and therefore wins
2701/// method resolution whenever the field type implements `ApiModel`. Otherwise
2702/// resolution falls through to the reference impl, which does nothing.
2703fn nested_probe_definitions() -> proc_macro2::TokenStream {
2704    let value = value_probe_definitions();
2705    let items = items_probe_definitions();
2706    let kind = kind_probe_definitions();
2707    let constrained = constrained_probe_definitions();
2708    quote! {
2709        #[allow(dead_code)]
2710        struct __BlazinglyValue<'probe, T>(&'probe T);
2711        #[allow(dead_code)]
2712        struct __BlazinglyItems<'probe, T>(&'probe [T]);
2713        #[allow(dead_code)]
2714        struct __BlazinglyKind<T>(::core::marker::PhantomData<T>);
2715
2716        #value
2717        #items
2718        #kind
2719        #constrained
2720    }
2721}
2722
2723/// Emits the probes that carry a declared value type's rules into the model.
2724///
2725/// A field written `title: Title` has to pick up the rules `Title` declared,
2726/// both in the descriptor and in the validation pass, without the model
2727/// knowing at expansion time whether `Title` declares any.
2728fn constrained_probe_definitions() -> proc_macro2::TokenStream {
2729    let rules = declared_rules_probe_definitions();
2730    let checks = constrained_check_probe_definitions();
2731    quote! {
2732        #rules
2733        #checks
2734    }
2735}
2736
2737fn declared_rules_probe_definitions() -> proc_macro2::TokenStream {
2738    quote! {
2739        #[allow(dead_code)]
2740        trait __BlazinglyDeclaredRules {
2741            fn __blazingly_declared_rules(
2742                &self,
2743            ) -> ::std::vec::Vec<::blazingly::ValidationRule>;
2744        }
2745
2746        impl<T: ::blazingly::ApiConstrained> __BlazinglyDeclaredRules for __BlazinglyKind<T> {
2747            fn __blazingly_declared_rules(
2748                &self,
2749            ) -> ::std::vec::Vec<::blazingly::ValidationRule> {
2750                <T as ::blazingly::ApiConstrained>::constraint_rules()
2751            }
2752        }
2753
2754        #[allow(dead_code)]
2755        trait __BlazinglyPlainRules {
2756            fn __blazingly_declared_rules(
2757                &self,
2758            ) -> ::std::vec::Vec<::blazingly::ValidationRule>;
2759        }
2760
2761        impl<T> __BlazinglyPlainRules for &__BlazinglyKind<T> {
2762            fn __blazingly_declared_rules(
2763                &self,
2764            ) -> ::std::vec::Vec<::blazingly::ValidationRule> {
2765                ::std::vec::Vec::new()
2766            }
2767        }
2768    }
2769}
2770
2771fn constrained_check_probe_definitions() -> proc_macro2::TokenStream {
2772    quote! {
2773        #[allow(dead_code)]
2774        trait __BlazinglyConstrainedValue {
2775            fn __blazingly_constrained(
2776                &self,
2777                errors: &mut ::blazingly::ValidationErrors,
2778                field: &str,
2779            );
2780        }
2781
2782        impl<T: ::blazingly::ApiConstrained> __BlazinglyConstrainedValue
2783            for __BlazinglyValue<'_, T>
2784        {
2785            fn __blazingly_constrained(
2786                &self,
2787                errors: &mut ::blazingly::ValidationErrors,
2788                field: &str,
2789            ) {
2790                if let ::core::result::Result::Err(declared) =
2791                    ::blazingly::ApiConstrained::validate_constraints(self.0)
2792                {
2793                    ::blazingly::merge_field_validation_errors(errors, field, &declared);
2794                }
2795            }
2796        }
2797
2798        #[allow(dead_code)]
2799        trait __BlazinglyPlainConstrained {
2800            fn __blazingly_constrained(
2801                &self,
2802                errors: &mut ::blazingly::ValidationErrors,
2803                field: &str,
2804            );
2805        }
2806
2807        impl<T> __BlazinglyPlainConstrained for &__BlazinglyValue<'_, T> {
2808            fn __blazingly_constrained(
2809                &self,
2810                _errors: &mut ::blazingly::ValidationErrors,
2811                _field: &str,
2812            ) {
2813            }
2814        }
2815
2816        #[allow(dead_code)]
2817        trait __BlazinglyConstrainedItems {
2818            fn __blazingly_constrained_items(
2819                &self,
2820                errors: &mut ::blazingly::ValidationErrors,
2821                field: &str,
2822            );
2823        }
2824
2825        impl<T: ::blazingly::ApiConstrained> __BlazinglyConstrainedItems
2826            for __BlazinglyItems<'_, T>
2827        {
2828            fn __blazingly_constrained_items(
2829                &self,
2830                errors: &mut ::blazingly::ValidationErrors,
2831                field: &str,
2832            ) {
2833                for (index, item) in self.0.iter().enumerate() {
2834                    if let ::core::result::Result::Err(declared) =
2835                        ::blazingly::ApiConstrained::validate_constraints(item)
2836                    {
2837                        let prefix = ::std::format!("{}[{}]", field, index);
2838                        ::blazingly::merge_field_validation_errors(errors, &prefix, &declared);
2839                    }
2840                }
2841            }
2842        }
2843
2844        #[allow(dead_code)]
2845        trait __BlazinglyPlainConstrainedItems {
2846            fn __blazingly_constrained_items(
2847                &self,
2848                errors: &mut ::blazingly::ValidationErrors,
2849                field: &str,
2850            );
2851        }
2852
2853        impl<T> __BlazinglyPlainConstrainedItems for &__BlazinglyItems<'_, T> {
2854            fn __blazingly_constrained_items(
2855                &self,
2856                _errors: &mut ::blazingly::ValidationErrors,
2857                _field: &str,
2858            ) {
2859            }
2860        }
2861    }
2862}
2863
2864fn value_probe_definitions() -> proc_macro2::TokenStream {
2865    quote! {
2866        #[allow(dead_code)]
2867        trait __BlazinglyNestedValue {
2868            fn __blazingly_nested(
2869                &self,
2870                errors: &mut ::blazingly::ValidationErrors,
2871                field: &str,
2872            );
2873        }
2874
2875        impl<T: ::blazingly::ApiModel> __BlazinglyNestedValue for __BlazinglyValue<'_, T> {
2876            fn __blazingly_nested(
2877                &self,
2878                errors: &mut ::blazingly::ValidationErrors,
2879                field: &str,
2880            ) {
2881                if let ::core::result::Result::Err(nested) =
2882                    ::blazingly::ApiModel::validate(self.0)
2883                {
2884                    ::blazingly::merge_validation_errors(errors, field, &nested);
2885                }
2886            }
2887        }
2888
2889        #[allow(dead_code)]
2890        trait __BlazinglyPlainValue {
2891            fn __blazingly_nested(
2892                &self,
2893                errors: &mut ::blazingly::ValidationErrors,
2894                field: &str,
2895            );
2896        }
2897
2898        impl<T> __BlazinglyPlainValue for &__BlazinglyValue<'_, T> {
2899            fn __blazingly_nested(
2900                &self,
2901                _errors: &mut ::blazingly::ValidationErrors,
2902                _field: &str,
2903            ) {
2904            }
2905        }
2906    }
2907}
2908
2909fn items_probe_definitions() -> proc_macro2::TokenStream {
2910    quote! {
2911        #[allow(dead_code)]
2912        trait __BlazinglyNestedItems {
2913            fn __blazingly_nested_items(
2914                &self,
2915                errors: &mut ::blazingly::ValidationErrors,
2916                field: &str,
2917            );
2918        }
2919
2920        impl<T: ::blazingly::ApiModel> __BlazinglyNestedItems for __BlazinglyItems<'_, T> {
2921            fn __blazingly_nested_items(
2922                &self,
2923                errors: &mut ::blazingly::ValidationErrors,
2924                field: &str,
2925            ) {
2926                for (index, item) in self.0.iter().enumerate() {
2927                    if let ::core::result::Result::Err(nested) =
2928                        ::blazingly::ApiModel::validate(item)
2929                    {
2930                        let prefix = ::std::format!("{}[{}]", field, index);
2931                        ::blazingly::merge_validation_errors(errors, &prefix, &nested);
2932                    }
2933                }
2934            }
2935        }
2936
2937        #[allow(dead_code)]
2938        trait __BlazinglyPlainItems {
2939            fn __blazingly_nested_items(
2940                &self,
2941                errors: &mut ::blazingly::ValidationErrors,
2942                field: &str,
2943            );
2944        }
2945
2946        impl<T> __BlazinglyPlainItems for &__BlazinglyItems<'_, T> {
2947            fn __blazingly_nested_items(
2948                &self,
2949                _errors: &mut ::blazingly::ValidationErrors,
2950                _field: &str,
2951            ) {
2952            }
2953        }
2954    }
2955}
2956
2957fn kind_probe_definitions() -> proc_macro2::TokenStream {
2958    quote! {
2959        #[allow(dead_code)]
2960        trait __BlazinglyModelKind {
2961            fn __blazingly_is_model(&self) -> bool;
2962        }
2963
2964        impl<T: ::blazingly::ApiModel> __BlazinglyModelKind for __BlazinglyKind<T> {
2965            fn __blazingly_is_model(&self) -> bool {
2966                true
2967            }
2968        }
2969
2970        #[allow(dead_code)]
2971        trait __BlazinglyPlainKind {
2972            fn __blazingly_is_model(&self) -> bool;
2973        }
2974
2975        impl<T> __BlazinglyPlainKind for &__BlazinglyKind<T> {
2976            fn __blazingly_is_model(&self) -> bool {
2977                false
2978            }
2979        }
2980    }
2981}
2982
2983fn nested_rule_descriptor(
2984    validation_type: &Type,
2985    shape: FieldShape,
2986    explicit: bool,
2987) -> proc_macro2::TokenStream {
2988    if explicit {
2989        return quote!(rules.push(::blazingly::ValidationRule::Nested););
2990    }
2991    if !shape.may_be_model() {
2992        return quote!();
2993    }
2994    let probe_type = model_probe_type(validation_type, shape);
2995    quote! {
2996        if (&__BlazinglyKind::<#probe_type>(::core::marker::PhantomData))
2997            .__blazingly_is_model()
2998        {
2999            rules.push(::blazingly::ValidationRule::Nested);
3000        }
3001    }
3002}
3003
3004/// Copies a declared value type's rules into the field that uses it.
3005///
3006/// Only a scalar-shaped field inherits: a `Vec<Title>` field would otherwise
3007/// claim the item's bounds as its own. A collection needs no copy at all — the
3008/// item's `TypeDescriptor` carries the rules the type itself declared.
3009fn inherited_rule_descriptor(
3010    validation_type: &Type,
3011    shape: FieldShape,
3012) -> proc_macro2::TokenStream {
3013    if shape != FieldShape::Other {
3014        return quote!();
3015    }
3016    quote! {
3017        rules.extend(
3018            (&__BlazinglyKind::<#validation_type>(::core::marker::PhantomData))
3019                .__blazingly_declared_rules(),
3020        );
3021    }
3022}
3023
3024fn model_probe_type(validation_type: &Type, shape: FieldShape) -> Type {
3025    if shape == FieldShape::Collection {
3026        wrapper_inner(validation_type, "Vec").unwrap_or_else(|| validation_type.clone())
3027    } else {
3028        validation_type.clone()
3029    }
3030}
3031
3032fn model_rename_rule(arguments: &ModelArgs) -> syn::Result<String> {
3033    let rename_rule = arguments
3034        .rename_all
3035        .as_ref()
3036        .map_or_else(|| "none".to_owned(), LitStr::value);
3037    if matches!(rename_rule.as_str(), "none" | "camelCase") {
3038        return Ok(rename_rule);
3039    }
3040
3041    Err(syn::Error::new(
3042        arguments
3043            .rename_all
3044            .as_ref()
3045            .map_or_else(proc_macro2::Span::call_site, LitStr::span),
3046        "the first milestone supports only `rename_all = \"camelCase\"`",
3047    ))
3048}
3049
3050#[allow(clippy::too_many_lines)]
3051fn take_field_rules(attributes: &mut Vec<Attribute>) -> syn::Result<FieldRules> {
3052    let mut retained = Vec::new();
3053    let mut rules = FieldRules::default();
3054
3055    for attribute in attributes.drain(..) {
3056        let path = attribute.path().clone();
3057        if path.is_ident("min_length") {
3058            let value = attribute.parse_args::<LitInt>()?;
3059            rules.min_length = Some((value.base10_parse()?, value.span()));
3060        } else if path.is_ident("max_length") {
3061            let value = attribute.parse_args::<LitInt>()?;
3062            rules.max_length = Some((value.base10_parse()?, value.span()));
3063        } else if path.is_ident("min_items") {
3064            let value = attribute.parse_args::<LitInt>()?;
3065            rules.min_items = Some((value.base10_parse()?, value.span()));
3066        } else if path.is_ident("max_items") {
3067            let value = attribute.parse_args::<LitInt>()?;
3068            rules.max_items = Some((value.base10_parse()?, value.span()));
3069        } else if path.is_ident("unique_items") {
3070            rules.unique_items = Some(attribute_span(&attribute));
3071        } else if path.is_ident("minimum") {
3072            rules.minimum = Some(numeric_attribute(&attribute)?);
3073        } else if path.is_ident("maximum") {
3074            rules.maximum = Some(numeric_attribute(&attribute)?);
3075        } else if path.is_ident("exclusive_minimum") {
3076            rules.exclusive_minimum = Some(numeric_attribute(&attribute)?);
3077        } else if path.is_ident("exclusive_maximum") {
3078            rules.exclusive_maximum = Some(numeric_attribute(&attribute)?);
3079        } else if path.is_ident("multiple_of") {
3080            let (factor, span) = numeric_attribute(&attribute)?;
3081            if factor.is_zero() {
3082                return Err(syn::Error::new(span, "`multiple_of` cannot be zero"));
3083            }
3084            rules.multiple_of = Some((factor, span));
3085        } else if path.is_ident("pattern") {
3086            let pattern = attribute.parse_args::<LitStr>()?;
3087            if let Err(reason) = lint_pattern_syntax(&pattern.value()) {
3088                return Err(syn::Error::new(pattern.span(), reason));
3089            }
3090            rules.pattern = Some(pattern);
3091        } else if path.is_ident("default") {
3092            if rules.default.is_some() {
3093                return Err(syn::Error::new_spanned(
3094                    attribute,
3095                    "only one `default` may be declared per field",
3096                ));
3097            }
3098            rules.default = Some(default_attribute(&attribute)?);
3099        } else if path.is_ident("email") {
3100            rules.email = Some(attribute_span(&attribute));
3101        } else if path.is_ident("alias") {
3102            rules.aliases.push(attribute.parse_args::<LitStr>()?);
3103        } else if path.is_ident("validate_with") {
3104            if rules.validator.is_some() {
3105                return Err(syn::Error::new_spanned(
3106                    attribute,
3107                    "only one `validate_with` function may be declared per field",
3108                ));
3109            }
3110            rules.validator = Some(attribute.parse_args::<SynPath>()?);
3111        } else if path.is_ident("nested") {
3112            rules.nested = true;
3113        } else {
3114            retained.push(attribute);
3115        }
3116    }
3117
3118    reject_inverted_bounds(&rules)?;
3119
3120    *attributes = retained;
3121    Ok(rules)
3122}
3123
3124fn attribute_span(attribute: &Attribute) -> proc_macro2::Span {
3125    attribute
3126        .path()
3127        .segments
3128        .last()
3129        .map_or_else(proc_macro2::Span::call_site, |segment| segment.ident.span())
3130}
3131
3132fn default_attribute(attribute: &Attribute) -> syn::Result<(DefaultLiteral, proc_macro2::Span)> {
3133    let expression = attribute.parse_args::<syn::Expr>()?;
3134    let unsupported = || {
3135        syn::Error::new_spanned(
3136            &expression,
3137            "`default` requires a string, integer, floating-point, or boolean literal",
3138        )
3139    };
3140    let (negative, literal) = match &expression {
3141        syn::Expr::Lit(literal) => (false, &literal.lit),
3142        syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
3143            let syn::Expr::Lit(literal) = unary.expr.as_ref() else {
3144                return Err(unsupported());
3145            };
3146            (true, &literal.lit)
3147        }
3148        _ => return Err(unsupported()),
3149    };
3150    let span = literal.span();
3151    let value = match literal {
3152        syn::Lit::Str(value) if !negative => DefaultLiteral::Text(value.clone()),
3153        syn::Lit::Bool(value) if !negative => DefaultLiteral::Boolean(value.clone()),
3154        syn::Lit::Int(value) => {
3155            let magnitude = value.base10_parse::<i128>()?;
3156            DefaultLiteral::Number(NumericLiteral::Integer(if negative {
3157                -magnitude
3158            } else {
3159                magnitude
3160            }))
3161        }
3162        syn::Lit::Float(value) => {
3163            let magnitude = value.base10_parse::<f64>()?;
3164            let magnitude = if negative { -magnitude } else { magnitude };
3165            if !magnitude.is_finite() {
3166                return Err(syn::Error::new(span, "a `default` must be finite"));
3167            }
3168            DefaultLiteral::Number(NumericLiteral::Float(magnitude))
3169        }
3170        _ => return Err(unsupported()),
3171    };
3172    Ok((value, span))
3173}
3174
3175fn numeric_attribute(attribute: &Attribute) -> syn::Result<(NumericLiteral, proc_macro2::Span)> {
3176    let expression = attribute.parse_args::<syn::Expr>()?;
3177    let (negative, literal) = match &expression {
3178        syn::Expr::Lit(literal) => (false, &literal.lit),
3179        syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
3180            let syn::Expr::Lit(literal) = unary.expr.as_ref() else {
3181                return Err(syn::Error::new_spanned(
3182                    &expression,
3183                    "numeric bounds require an integer or floating-point literal",
3184                ));
3185            };
3186            (true, &literal.lit)
3187        }
3188        _ => {
3189            return Err(syn::Error::new_spanned(
3190                &expression,
3191                "numeric bounds require an integer or floating-point literal",
3192            ));
3193        }
3194    };
3195    let span = literal.span();
3196    let value = match literal {
3197        syn::Lit::Int(value) => {
3198            let magnitude = value.base10_parse::<i128>()?;
3199            NumericLiteral::Integer(if negative { -magnitude } else { magnitude })
3200        }
3201        syn::Lit::Float(value) => {
3202            let magnitude = value.base10_parse::<f64>()?;
3203            let magnitude = if negative { -magnitude } else { magnitude };
3204            if !magnitude.is_finite() {
3205                return Err(syn::Error::new(span, "numeric bounds must be finite"));
3206            }
3207            NumericLiteral::Float(magnitude)
3208        }
3209        _ => {
3210            return Err(syn::Error::new(
3211                span,
3212                "numeric bounds require an integer or floating-point literal",
3213            ));
3214        }
3215    };
3216    Ok((value, span))
3217}
3218
3219fn reject_inverted_bounds(rules: &FieldRules) -> syn::Result<()> {
3220    if let (Some((minimum, span)), Some((maximum, _))) = (rules.min_length, rules.max_length)
3221        && minimum > maximum
3222    {
3223        return Err(syn::Error::new(
3224            span,
3225            "`min_length` cannot be greater than `max_length`",
3226        ));
3227    }
3228    if let (Some((minimum, span)), Some((maximum, _))) = (rules.min_items, rules.max_items)
3229        && minimum > maximum
3230    {
3231        return Err(syn::Error::new(
3232            span,
3233            "`min_items` cannot be greater than `max_items`",
3234        ));
3235    }
3236    if let (Some((minimum, span)), Some((maximum, _))) = (rules.minimum, rules.maximum)
3237        && minimum.exceeds(maximum)
3238    {
3239        return Err(syn::Error::new(
3240            span,
3241            "`minimum` cannot be greater than `maximum`",
3242        ));
3243    }
3244    if let (Some((minimum, span)), Some((maximum, _))) =
3245        (rules.exclusive_minimum, rules.exclusive_maximum)
3246        && minimum.exceeds(maximum)
3247    {
3248        return Err(syn::Error::new(
3249            span,
3250            "`exclusive_minimum` cannot be greater than `exclusive_maximum`",
3251        ));
3252    }
3253    Ok(())
3254}
3255
3256fn reject_incompatible_rules(
3257    rules: &FieldRules,
3258    validation_type: &Type,
3259    shape: FieldShape,
3260) -> syn::Result<()> {
3261    let numeric = [
3262        (rules.minimum.map(|(_, span)| span), "minimum"),
3263        (rules.maximum.map(|(_, span)| span), "maximum"),
3264        (
3265            rules.exclusive_minimum.map(|(_, span)| span),
3266            "exclusive_minimum",
3267        ),
3268        (
3269            rules.exclusive_maximum.map(|(_, span)| span),
3270            "exclusive_maximum",
3271        ),
3272        (rules.multiple_of.map(|(_, span)| span), "multiple_of"),
3273    ];
3274    for (span, name) in numeric {
3275        if let Some(span) = span
3276            && !shape.is_numeric()
3277        {
3278            return Err(syn::Error::new(
3279                span,
3280                format!(
3281                    "`{name}` requires an integer or floating-point field, \
3282                     but `{}` is not numeric",
3283                    type_label(validation_type)
3284                ),
3285            ));
3286        }
3287    }
3288
3289    let collection = [
3290        (rules.min_items.map(|(_, span)| span), "min_items"),
3291        (rules.max_items.map(|(_, span)| span), "max_items"),
3292        (rules.unique_items, "unique_items"),
3293    ];
3294    for (span, name) in collection {
3295        if let Some(span) = span
3296            && shape != FieldShape::Collection
3297        {
3298            return Err(syn::Error::new(
3299                span,
3300                format!(
3301                    "`{name}` requires a `Vec<T>` or `Option<Vec<T>>` field, \
3302                     but `{}` is not a collection",
3303                    type_label(validation_type)
3304                ),
3305            ));
3306        }
3307    }
3308
3309    if let Some(pattern) = &rules.pattern
3310        && shape != FieldShape::Text
3311    {
3312        return Err(syn::Error::new(
3313            pattern.span(),
3314            format!(
3315                "`pattern` requires a `String` or `Option<String>` field, \
3316                 but `{}` is not a string",
3317                type_label(validation_type)
3318            ),
3319        ));
3320    }
3321
3322    if let Some(span) = rules.email
3323        && shape != FieldShape::Text
3324    {
3325        return Err(syn::Error::new(
3326            span,
3327            format!(
3328                "`email` requires a `String` or `Option<String>` field, \
3329                 but `{}` is not a string",
3330                type_label(validation_type)
3331            ),
3332        ));
3333    }
3334
3335    reject_incompatible_default(rules, validation_type, shape)?;
3336
3337    let lengths = [
3338        (rules.min_length.map(|(_, span)| span), "min_length"),
3339        (rules.max_length.map(|(_, span)| span), "max_length"),
3340    ];
3341    for (span, name) in lengths {
3342        if let Some(span) = span
3343            && !matches!(shape, FieldShape::Text | FieldShape::Collection)
3344        {
3345            return Err(syn::Error::new(
3346                span,
3347                format!(
3348                    "`{name}` requires a `String`, `Option<String>`, or `Vec<T>` field, \
3349                     but `{}` is neither",
3350                    type_label(validation_type)
3351                ),
3352            ));
3353        }
3354    }
3355
3356    Ok(())
3357}
3358
3359/// A default is substituted for the field itself, so it has to be the field's
3360/// own type; nothing here converts.
3361fn reject_incompatible_default(
3362    rules: &FieldRules,
3363    validation_type: &Type,
3364    shape: FieldShape,
3365) -> syn::Result<()> {
3366    let Some((literal, span)) = &rules.default else {
3367        return Ok(());
3368    };
3369    let accepted = match literal {
3370        DefaultLiteral::Text(_) => shape == FieldShape::Text,
3371        DefaultLiteral::Number(NumericLiteral::Integer(_)) => shape.is_numeric(),
3372        DefaultLiteral::Number(NumericLiteral::Float(_)) => shape == FieldShape::Float,
3373        DefaultLiteral::Boolean(_) => bare_type_matches(validation_type, &["bool"]),
3374    };
3375    if accepted {
3376        return Ok(());
3377    }
3378    let (written, expected) = literal.expectation();
3379    Err(syn::Error::new(
3380        *span,
3381        format!(
3382            "`default` with {written} requires {expected}, but `{}` is not one",
3383            type_label(validation_type)
3384        ),
3385    ))
3386}
3387
3388/// Folds `min_length`/`max_length` into the item bounds for collection fields.
3389fn normalize_collection_rules(rules: &mut FieldRules, shape: FieldShape) {
3390    if shape != FieldShape::Collection {
3391        return;
3392    }
3393    if rules.min_items.is_none() {
3394        rules.min_items = rules.min_length;
3395    }
3396    if rules.max_items.is_none() {
3397        rules.max_items = rules.max_length;
3398    }
3399    rules.min_length = None;
3400    rules.max_length = None;
3401}
3402
3403fn rule_descriptors(rules: &FieldRules, nullable: bool) -> Vec<proc_macro2::TokenStream> {
3404    let mut descriptors = Vec::new();
3405
3406    if let Some((minimum, _)) = rules.min_length {
3407        descriptors.push(quote!(::blazingly::ValidationRule::MinLength(#minimum)));
3408    }
3409    if let Some((maximum, _)) = rules.max_length {
3410        descriptors.push(quote!(::blazingly::ValidationRule::MaxLength(#maximum)));
3411    }
3412    if rules.email.is_some() {
3413        descriptors.push(quote!(::blazingly::ValidationRule::Email));
3414    }
3415    for encoded in constraint_encodings(rules) {
3416        descriptors.push(quote!(::blazingly::ValidationRule::Custom(#encoded.to_owned())));
3417    }
3418    for encoded in metadata_encodings(rules, nullable) {
3419        descriptors.push(quote!(::blazingly::ValidationRule::Custom(#encoded.to_owned())));
3420    }
3421    for alias in &rules.aliases {
3422        descriptors.push(quote!(::blazingly::ValidationRule::Alias(#alias.to_owned())));
3423    }
3424    if let Some(validator) = &rules.validator {
3425        descriptors.push(quote!(::blazingly::ValidationRule::Custom(
3426            stringify!(#validator).to_owned()
3427        )));
3428    }
3429
3430    descriptors
3431}
3432
3433/// Encodes constraints without a dedicated contract variant as `key=value`.
3434fn constraint_encodings(rules: &FieldRules) -> Vec<String> {
3435    let mut encodings = Vec::new();
3436    let numeric = [
3437        (rules.minimum, "minimum"),
3438        (rules.maximum, "maximum"),
3439        (rules.exclusive_minimum, "exclusive_minimum"),
3440        (rules.exclusive_maximum, "exclusive_maximum"),
3441        (rules.multiple_of, "multiple_of"),
3442    ];
3443    for (rule, keyword) in numeric {
3444        if let Some((value, _)) = rule {
3445            encodings.push(format!("{keyword}={}", value.encoded()));
3446        }
3447    }
3448    if let Some(pattern) = &rules.pattern {
3449        encodings.push(format!("pattern={}", pattern.value()));
3450    }
3451    if let Some((minimum, _)) = rules.min_items {
3452        encodings.push(format!("min_items={minimum}"));
3453    }
3454    if let Some((maximum, _)) = rules.max_items {
3455        encodings.push(format!("max_items={maximum}"));
3456    }
3457    if rules.unique_items.is_some() {
3458        encodings.push("unique_items=true".to_owned());
3459    }
3460    encodings
3461}
3462
3463/// Encodes field metadata the frozen contract format cannot name.
3464///
3465/// `blazingly::FieldMetadata` is the reader; the `keyword=value` channel is the
3466/// one already used for the constraints above.
3467fn metadata_encodings(rules: &FieldRules, nullable: bool) -> Vec<String> {
3468    let mut encodings = Vec::new();
3469    if let Some((literal, _)) = &rules.default {
3470        encodings.push(format!("default={}", literal.encoded()));
3471    }
3472    if nullable {
3473        encodings.push("nullable=true".to_owned());
3474    }
3475    encodings
3476}
3477
3478fn field_checks(
3479    rules: &FieldRules,
3480    shape: FieldShape,
3481    public_name: &LitStr,
3482) -> proc_macro2::TokenStream {
3483    match shape {
3484        FieldShape::Text => text_checks(rules, public_name),
3485        FieldShape::Integer | FieldShape::Float => numeric_checks(rules, public_name),
3486        FieldShape::Collection => collection_checks(rules, public_name),
3487        FieldShape::Other => quote!(),
3488    }
3489}
3490
3491fn text_checks(rules: &FieldRules, public_name: &LitStr) -> proc_macro2::TokenStream {
3492    let minimum = rules.min_length.map(|(minimum, _)| {
3493        quote! {
3494            if value.chars().count() < #minimum {
3495                errors.push(
3496                    #public_name,
3497                    "min_length",
3498                    ::std::format!("must contain at least {} characters", #minimum),
3499                );
3500            }
3501        }
3502    });
3503    let maximum = rules.max_length.map(|(maximum, _)| {
3504        quote! {
3505            if value.chars().count() > #maximum {
3506                errors.push(
3507                    #public_name,
3508                    "max_length",
3509                    ::std::format!("must contain at most {} characters", #maximum),
3510                );
3511            }
3512        }
3513    });
3514    let email = rules.email.map(|_| {
3515        quote! {
3516            if !::blazingly::is_email(value) {
3517                errors.push(
3518                    #public_name,
3519                    "email",
3520                    "must be a valid email address",
3521                );
3522            }
3523        }
3524    });
3525    let pattern = rules.pattern.as_ref().map(|pattern| {
3526        quote! {
3527            ::blazingly::validation::check_pattern(
3528                &mut errors,
3529                #public_name,
3530                value.as_str(),
3531                #pattern,
3532            );
3533        }
3534    });
3535
3536    quote! {
3537        #minimum
3538        #maximum
3539        #email
3540        #pattern
3541    }
3542}
3543
3544fn numeric_checks(rules: &FieldRules, public_name: &LitStr) -> proc_macro2::TokenStream {
3545    let checks = [
3546        (rules.minimum, quote!(check_minimum)),
3547        (rules.maximum, quote!(check_maximum)),
3548        (rules.exclusive_minimum, quote!(check_exclusive_minimum)),
3549        (rules.exclusive_maximum, quote!(check_exclusive_maximum)),
3550        (rules.multiple_of, quote!(check_multiple_of)),
3551    ]
3552    .into_iter()
3553    .filter_map(|(rule, function)| {
3554        let (bound, _) = rule?;
3555        let bound = bound.tokens();
3556        Some(quote! {
3557            ::blazingly::validation::#function(&mut errors, #public_name, *value, #bound);
3558        })
3559    });
3560
3561    quote!(#(#checks)*)
3562}
3563
3564fn collection_checks(rules: &FieldRules, public_name: &LitStr) -> proc_macro2::TokenStream {
3565    let minimum = rules.min_items.map(|(minimum, _)| {
3566        quote! {
3567            ::blazingly::validation::check_min_items(
3568                &mut errors,
3569                #public_name,
3570                value.as_slice(),
3571                #minimum,
3572            );
3573        }
3574    });
3575    let maximum = rules.max_items.map(|(maximum, _)| {
3576        quote! {
3577            ::blazingly::validation::check_max_items(
3578                &mut errors,
3579                #public_name,
3580                value.as_slice(),
3581                #maximum,
3582            );
3583        }
3584    });
3585    let unique = rules.unique_items.map(|_| {
3586        quote! {
3587            ::blazingly::validation::check_unique_items(
3588                &mut errors,
3589                #public_name,
3590                value.as_slice(),
3591            );
3592        }
3593    });
3594
3595    quote! {
3596        #minimum
3597        #maximum
3598        #unique
3599    }
3600}
3601
3602fn nested_validation_checks(
3603    shape: FieldShape,
3604    public_name: &LitStr,
3605    rules: &FieldRules,
3606) -> proc_macro2::TokenStream {
3607    let nested = shape.may_be_model().then(|| {
3608        if shape == FieldShape::Collection {
3609            quote! {
3610                (&__BlazinglyItems(value.as_slice()))
3611                    .__blazingly_nested_items(&mut errors, #public_name);
3612                (&__BlazinglyItems(value.as_slice()))
3613                    .__blazingly_constrained_items(&mut errors, #public_name);
3614            }
3615        } else {
3616            quote! {
3617                (&__BlazinglyValue(value)).__blazingly_nested(&mut errors, #public_name);
3618                (&__BlazinglyValue(value)).__blazingly_constrained(&mut errors, #public_name);
3619            }
3620        }
3621    });
3622    let custom = rules.validator.as_ref().map(|validator| {
3623        quote! {
3624            if let ::core::result::Result::Err(custom_errors) = #validator(value) {
3625                ::blazingly::merge_field_validation_errors(
3626                    &mut errors,
3627                    #public_name,
3628                    &custom_errors,
3629                );
3630            }
3631        }
3632    });
3633    quote! {
3634        #nested
3635        #custom
3636    }
3637}
3638
3639const INTEGER_TYPES: &[&str] = &[
3640    "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
3641];
3642
3643const FLOAT_TYPES: &[&str] = &["f32", "f64"];
3644
3645fn field_shape(ty: &Type) -> FieldShape {
3646    if is_string_type(ty) {
3647        return FieldShape::Text;
3648    }
3649    if bare_type_matches(ty, INTEGER_TYPES) {
3650        return FieldShape::Integer;
3651    }
3652    if bare_type_matches(ty, FLOAT_TYPES) {
3653        return FieldShape::Float;
3654    }
3655    if wrapper_inner(ty, "Vec").is_some() {
3656        return FieldShape::Collection;
3657    }
3658    FieldShape::Other
3659}
3660
3661fn bare_type_matches(ty: &Type, names: &[&str]) -> bool {
3662    let Type::Path(path) = ty else {
3663        return false;
3664    };
3665    path.path.segments.last().is_some_and(|segment| {
3666        segment.arguments.is_none() && names.contains(&segment.ident.to_string().as_str())
3667    })
3668}
3669
3670fn type_label(ty: &Type) -> String {
3671    let Type::Path(path) = ty else {
3672        return "this field type".to_owned();
3673    };
3674    path.path.segments.last().map_or_else(
3675        || "this field type".to_owned(),
3676        |segment| segment.ident.to_string(),
3677    )
3678}
3679
3680fn is_string_type(ty: &Type) -> bool {
3681    let Type::Path(path) = ty else {
3682        return false;
3683    };
3684    path.path
3685        .segments
3686        .last()
3687        .is_some_and(|segment| segment.ident == "String")
3688}
3689
3690/// Maximum pattern length accepted by `blazingly_validation::Pattern`.
3691const MAX_PATTERN_CHARS: usize = 512;
3692
3693/// Maximum group nesting accepted by `blazingly_validation::Pattern`.
3694const MAX_PATTERN_DEPTH: i32 = 16;
3695
3696const fn is_supported_escape(value: char) -> bool {
3697    matches!(
3698        value,
3699        'd' | 'D'
3700            | 'w'
3701            | 'W'
3702            | 's'
3703            | 'S'
3704            | 't'
3705            | 'n'
3706            | 'r'
3707            | '\\'
3708            | '.'
3709            | '*'
3710            | '+'
3711            | '?'
3712            | '('
3713            | ')'
3714            | '['
3715            | ']'
3716            | '{'
3717            | '}'
3718            | '|'
3719            | '^'
3720            | '$'
3721            | '-'
3722            | '/'
3723    )
3724}
3725
3726/// Rejects patterns outside the runtime matcher's supported subset.
3727///
3728/// The runtime matcher stays authoritative; this check exists so the common
3729/// mistakes surface at compile time instead of as a violation on every request.
3730#[allow(clippy::too_many_lines)]
3731fn lint_pattern_syntax(pattern: &str) -> Result<(), String> {
3732    if pattern.is_empty() {
3733        return Err("the pattern is empty".to_owned());
3734    }
3735    let characters = pattern.chars().collect::<Vec<_>>();
3736    if characters.len() > MAX_PATTERN_CHARS {
3737        return Err(format!(
3738            "the pattern exceeds {MAX_PATTERN_CHARS} characters"
3739        ));
3740    }
3741    let last = characters.len() - 1;
3742    let mut depth = 0_i32;
3743    let mut deepest = 0_i32;
3744    let mut in_class = false;
3745    let mut quantifiable = false;
3746    let mut index = 0;
3747
3748    while let Some(&value) = characters.get(index) {
3749        match value {
3750            '\\' => {
3751                let Some(&escaped) = characters.get(index + 1) else {
3752                    return Err("the pattern ends with a lone backslash".to_owned());
3753                };
3754                if !is_supported_escape(escaped) {
3755                    return Err(format!("the escape `\\{escaped}` is not supported"));
3756                }
3757                quantifiable = true;
3758                index += 2;
3759                continue;
3760            }
3761            '[' if !in_class => {
3762                in_class = true;
3763                quantifiable = false;
3764            }
3765            ']' if in_class => {
3766                in_class = false;
3767                quantifiable = true;
3768            }
3769            _ if in_class => {}
3770            '(' => {
3771                if characters.get(index + 1) == Some(&'?') {
3772                    return Err(
3773                        "group flags, lookaround, and non-capturing groups are not supported"
3774                            .to_owned(),
3775                    );
3776                }
3777                depth += 1;
3778                deepest = deepest.max(depth);
3779                quantifiable = false;
3780            }
3781            ')' => {
3782                depth -= 1;
3783                if depth < 0 {
3784                    return Err("the pattern has an unbalanced group".to_owned());
3785                }
3786                quantifiable = true;
3787            }
3788            '*' | '+' | '?' => {
3789                if !quantifiable {
3790                    return Err("a quantifier has no preceding expression".to_owned());
3791                }
3792                quantifiable = false;
3793            }
3794            '{' | '}' => {
3795                return Err("counted repetition `{m,n}` is not supported".to_owned());
3796            }
3797            '^' if index != 0 => {
3798                return Err("`^` is supported only at the start of a pattern".to_owned());
3799            }
3800            '$' if index != last => {
3801                return Err("`$` is supported only at the end of a pattern".to_owned());
3802            }
3803            '^' | '$' | '|' => quantifiable = false,
3804            _ => quantifiable = true,
3805        }
3806        index += 1;
3807    }
3808
3809    if in_class {
3810        return Err("the pattern has an unterminated character class".to_owned());
3811    }
3812    if depth != 0 {
3813        return Err("the pattern has an unbalanced group".to_owned());
3814    }
3815    if deepest > MAX_PATTERN_DEPTH {
3816        return Err(format!("groups nest deeper than {MAX_PATTERN_DEPTH}"));
3817    }
3818    Ok(())
3819}
3820
3821fn snake_to_camel(value: &str) -> String {
3822    let mut output = String::new();
3823    let mut uppercase = false;
3824
3825    for character in value.chars() {
3826        if character == '_' {
3827            uppercase = true;
3828        } else if uppercase {
3829            output.extend(character.to_uppercase());
3830            uppercase = false;
3831        } else {
3832            output.push(character);
3833        }
3834    }
3835
3836    output
3837}
3838
3839fn take_mcp_arguments(attributes: &mut Vec<Attribute>) -> syn::Result<Option<McpArgs>> {
3840    let Some(index) = attributes.iter().position(|attribute| {
3841        let segments = &attribute.path().segments;
3842        segments.len() == 2 && segments[0].ident == "mcp" && segments[1].ident == "tool"
3843    }) else {
3844        return Ok(None);
3845    };
3846
3847    let attribute = attributes.remove(index);
3848    attribute.parse_args().map(Some)
3849}
3850
3851fn take_security_arguments(attributes: &mut Vec<Attribute>) -> syn::Result<Vec<SecurityArgs>> {
3852    let mut parsed = Vec::new();
3853    let mut retained = Vec::with_capacity(attributes.len());
3854    for attribute in attributes.drain(..) {
3855        if attribute.path().is_ident("security") {
3856            parsed.push(attribute.parse_args()?);
3857        } else {
3858            retained.push(attribute);
3859        }
3860    }
3861    *attributes = retained;
3862    Ok(parsed)
3863}
3864
3865fn mcp_projection(
3866    arguments: Option<McpArgs>,
3867    function_name: &Ident,
3868    summary: &LitStr,
3869) -> syn::Result<proc_macro2::TokenStream> {
3870    let Some(arguments) = arguments else {
3871        return Ok(quote!(descriptor));
3872    };
3873
3874    let name = arguments
3875        .name
3876        .unwrap_or_else(|| LitStr::new(&function_name.to_string(), function_name.span()));
3877    let description = arguments.description.unwrap_or_else(|| summary.clone());
3878    let risk = enum_variant(
3879        arguments.risk.as_ref(),
3880        "read",
3881        &[
3882            ("read", quote!(::blazingly::OperationRisk::Read)),
3883            ("write", quote!(::blazingly::OperationRisk::Write)),
3884            (
3885                "destructive",
3886                quote!(::blazingly::OperationRisk::Destructive),
3887            ),
3888        ],
3889        "risk",
3890    )?;
3891    let confirmation = enum_variant(
3892        arguments.confirmation.as_ref(),
3893        "never",
3894        &[
3895            ("never", quote!(::blazingly::Confirmation::Never)),
3896            ("required", quote!(::blazingly::Confirmation::Required)),
3897        ],
3898        "confirmation",
3899    )?;
3900    let exposure = enum_variant(
3901        arguments.expose_output.as_ref(),
3902        "full",
3903        &[
3904            ("full", quote!(::blazingly::OutputExposure::Full)),
3905            (
3906                "summary_only",
3907                quote!(::blazingly::OutputExposure::SummaryOnly),
3908            ),
3909            ("none", quote!(::blazingly::OutputExposure::None)),
3910        ],
3911        "expose_output",
3912    )?;
3913    let idempotent = arguments
3914        .idempotent
3915        .map_or_else(|| quote!(false), |value| quote!(#value));
3916
3917    Ok(quote! {
3918        descriptor.with_mcp_tool(
3919            ::blazingly::McpToolDescriptor::new(#name, #description)
3920                .with_output_exposure(#exposure),
3921            ::blazingly::AgentPolicy {
3922                risk: #risk,
3923                confirmation: #confirmation,
3924                idempotent: #idempotent,
3925            },
3926        )
3927    })
3928}
3929
3930fn enum_variant(
3931    value: Option<&LitStr>,
3932    default: &str,
3933    variants: &[(&str, proc_macro2::TokenStream)],
3934    key: &str,
3935) -> syn::Result<proc_macro2::TokenStream> {
3936    let selected = value.map_or_else(|| default.to_owned(), LitStr::value);
3937    variants
3938        .iter()
3939        .find(|(name, _)| *name == selected)
3940        .map(|(_, tokens)| tokens.clone())
3941        .ok_or_else(|| {
3942            let message = format!("unsupported `{key}` value `{selected}`");
3943            let span = value.map_or_else(proc_macro2::Span::call_site, LitStr::span);
3944            syn::Error::new(span, message)
3945        })
3946}
3947
3948fn operation_inputs(
3949    inputs: &syn::punctuated::Punctuated<FnArg, Token![,]>,
3950) -> syn::Result<Vec<OperationInput>> {
3951    let mut operation_inputs = Vec::new();
3952    let mut body_inputs = 0;
3953
3954    for input in inputs {
3955        let FnArg::Typed(PatType { pat, ty, .. }) = input else {
3956            return Err(syn::Error::new_spanned(
3957                input,
3958                "methods with a `self` receiver are not supported",
3959            ));
3960        };
3961        let name = operation_argument_name(pat)?;
3962        let (declared, by_reference) = match &**ty {
3963            Type::Reference(reference) if reference.mutability.is_some() => {
3964                return Err(syn::Error::new_spanned(
3965                    ty,
3966                    "operation arguments cannot be taken by unique reference; a \
3967                     dependency is shared across the request",
3968                ));
3969            }
3970            Type::Reference(reference) => (&*reference.elem, true),
3971            other => (other, false),
3972        };
3973        let (kind, inner) = OperationInputKind::from_type(declared)
3974            .unwrap_or_else(|| (OperationInputKind::DirectDependency, declared.clone()));
3975        if by_reference && !kind.is_dependency() {
3976            return Err(syn::Error::new_spanned(
3977                ty,
3978                "only dependencies may be taken by reference; an extracted \
3979                 argument is decoded from the request and owns its data",
3980            ));
3981        }
3982        if matches!(
3983            kind,
3984            OperationInputKind::Json
3985                | OperationInputKind::Form
3986                | OperationInputKind::Multipart
3987                | OperationInputKind::File
3988                | OperationInputKind::Stream
3989        ) {
3990            body_inputs += 1;
3991            if body_inputs > 1 {
3992                return Err(syn::Error::new_spanned(
3993                    ty,
3994                    "an operation may declare only one body extractor",
3995                ));
3996            }
3997        }
3998        let required = wrapper_inner(&inner, "Option").is_none();
3999        if matches!(kind, OperationInputKind::Path) && !required {
4000            return Err(syn::Error::new_spanned(
4001                ty,
4002                "Path<T> arguments are always required and cannot wrap Option<T>",
4003            ));
4004        }
4005        operation_inputs.push(OperationInput {
4006            name: LitStr::new(&name.to_string(), name.span()),
4007            kind,
4008            argument_type: declared.clone(),
4009            inner,
4010            required,
4011            by_reference,
4012        });
4013    }
4014
4015    Ok(operation_inputs)
4016}
4017
4018fn operation_argument_name(pattern: &Pat) -> syn::Result<&Ident> {
4019    match pattern {
4020        Pat::Ident(pattern) => Ok(&pattern.ident),
4021        Pat::TupleStruct(pattern) if pattern.elems.len() == 1 => {
4022            let Some(Pat::Ident(pattern)) = pattern.elems.first() else {
4023                return Err(syn::Error::new_spanned(
4024                    pattern,
4025                    "extractor tuple patterns must contain one identifier",
4026                ));
4027            };
4028            Ok(&pattern.ident)
4029        }
4030        _ => Err(syn::Error::new_spanned(
4031            pattern,
4032            "operation arguments require an identifier or `Extractor(identifier)` pattern",
4033        )),
4034    }
4035}
4036
4037impl OperationInputKind {
4038    fn from_type(ty: &Type) -> Option<(Self, Type)> {
4039        if type_is(ty, "WebSocketRequest") {
4040            return Some((Self::WebSocket, ty.clone()));
4041        }
4042        if type_is(ty, "UploadBody") {
4043            return Some((Self::Stream, ty.clone()));
4044        }
4045        [
4046            (Self::Path, "Path"),
4047            (Self::Query, "Query"),
4048            (Self::Header, "Header"),
4049            (Self::Cookie, "Cookie"),
4050            (Self::Json, "Json"),
4051            (Self::Form, "Form"),
4052            (Self::Multipart, "Multipart"),
4053            (Self::File, "File"),
4054            (Self::Extension, "Extension"),
4055            (Self::Extract, "Extract"),
4056            (Self::Dependency, "Depends"),
4057        ]
4058        .into_iter()
4059        .find_map(|(kind, wrapper)| wrapper_inner(ty, wrapper).map(|inner| (kind, inner)))
4060    }
4061
4062    fn source_tokens(self) -> Option<proc_macro2::TokenStream> {
4063        match self {
4064            Self::Path => Some(quote!(::blazingly::InputSource::Path)),
4065            Self::Query => Some(quote!(::blazingly::InputSource::Query)),
4066            Self::Header => Some(quote!(::blazingly::InputSource::Header)),
4067            Self::Cookie => Some(quote!(::blazingly::InputSource::Cookie)),
4068            Self::Json => Some(quote!(::blazingly::InputSource::Json)),
4069            Self::Form => Some(quote!(::blazingly::InputSource::Form)),
4070            Self::Multipart => Some(quote!(::blazingly::InputSource::Multipart)),
4071            Self::File => Some(quote!(::blazingly::InputSource::File)),
4072            Self::Stream => Some(quote!(::blazingly::InputSource::Stream)),
4073            Self::WebSocket
4074            | Self::Extension
4075            | Self::Extract
4076            | Self::Dependency
4077            | Self::DirectDependency => None,
4078        }
4079    }
4080
4081    const fn is_dependency(self) -> bool {
4082        matches!(self, Self::Dependency | Self::DirectDependency)
4083    }
4084}
4085
4086fn operation_output(output: &ReturnType) -> syn::Result<OperationOutput> {
4087    let ReturnType::Type(_, ty) = output else {
4088        return Err(syn::Error::new_spanned(
4089            output,
4090            "an explicit typed response is required",
4091        ));
4092    };
4093
4094    if let Some((success, error)) = result_types(ty) {
4095        let (status, success) = success_output(&success)?;
4096        return Ok(OperationOutput {
4097            status,
4098            success,
4099            error: Some(error),
4100        });
4101    }
4102    let (status, success) = success_output(ty)?;
4103    Ok(OperationOutput {
4104        status,
4105        success,
4106        error: None,
4107    })
4108}
4109
4110fn success_output(ty: &Type) -> syn::Result<(u16, Option<Type>)> {
4111    if type_is(ty, "NoContent") {
4112        return Ok((204, None));
4113    }
4114    if let Some(inner) = wrapper_inner(ty, "WithHeaders") {
4115        return success_output(&inner);
4116    }
4117    if let Some(inner) = wrapper_inner(ty, "Background") {
4118        return success_output(&inner);
4119    }
4120    if let Some((status, inner)) = status_wrapper(ty)? {
4121        let (_, body) = success_output(&inner)?;
4122        if matches!(status, 204 | 304) && body.is_some() {
4123            return Err(syn::Error::new_spanned(
4124                ty,
4125                "HTTP status 204 and 304 responses cannot contain a body",
4126            ));
4127        }
4128        return Ok((status, body));
4129    }
4130    if let Some(inner) = wrapper_inner(ty, "Accepted") {
4131        return Ok((202, Some(inner)));
4132    }
4133    if let Some(inner) = wrapper_inner(ty, "Created") {
4134        return Ok((201, Some(inner)));
4135    }
4136    if let Some(inner) = wrapper_inner(ty, "Json") {
4137        return Ok((200, Some(inner)));
4138    }
4139    Ok((200, Some(ty.clone())))
4140}
4141
4142fn status_wrapper(ty: &Type) -> syn::Result<Option<(u16, Type)>> {
4143    let Type::Path(TypePath { path, .. }) = ty else {
4144        return Ok(None);
4145    };
4146    let Some(segment) = path.segments.last() else {
4147        return Ok(None);
4148    };
4149    if segment.ident != "Status" {
4150        return Ok(None);
4151    }
4152    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
4153        return Err(syn::Error::new_spanned(
4154            ty,
4155            "Status requires `Status<CODE, Response>`",
4156        ));
4157    };
4158    let mut arguments = arguments.args.iter();
4159    let Some(syn::GenericArgument::Const(syn::Expr::Lit(status))) = arguments.next() else {
4160        return Err(syn::Error::new_spanned(
4161            ty,
4162            "Status code must be an integer literal",
4163        ));
4164    };
4165    let syn::Lit::Int(status) = &status.lit else {
4166        return Err(syn::Error::new_spanned(
4167            status,
4168            "Status code must be an integer literal",
4169        ));
4170    };
4171    let status = status.base10_parse::<u16>()?;
4172    if !(200..=399).contains(&status) {
4173        return Err(syn::Error::new_spanned(
4174            ty,
4175            "typed success status must be between 200 and 399",
4176        ));
4177    }
4178    let Some(syn::GenericArgument::Type(inner)) = arguments.next() else {
4179        return Err(syn::Error::new_spanned(
4180            ty,
4181            "Status requires an inner typed response",
4182        ));
4183    };
4184    Ok(Some((status, inner.clone())))
4185}
4186
4187fn type_is(ty: &Type, expected: &str) -> bool {
4188    let Type::Path(TypePath { path, .. }) = ty else {
4189        return false;
4190    };
4191    path.segments
4192        .last()
4193        .is_some_and(|segment| segment.ident == expected)
4194}
4195
4196fn result_types(ty: &Type) -> Option<(Type, Type)> {
4197    let Type::Path(TypePath { path, .. }) = ty else {
4198        return None;
4199    };
4200    let segment = path.segments.last()?;
4201    if segment.ident != "Result" {
4202        return None;
4203    }
4204    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
4205        return None;
4206    };
4207    let mut types = arguments.args.iter().filter_map(|argument| {
4208        if let syn::GenericArgument::Type(ty) = argument {
4209            Some(ty.clone())
4210        } else {
4211            None
4212        }
4213    });
4214    Some((types.next()?, types.next()?))
4215}
4216
4217fn wrapper_inner(ty: &Type, wrapper: &str) -> Option<Type> {
4218    let Type::Path(TypePath { path, .. }) = ty else {
4219        return None;
4220    };
4221    let segment = path.segments.last()?;
4222    if segment.ident != wrapper {
4223        return None;
4224    }
4225    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
4226        return None;
4227    };
4228    let syn::GenericArgument::Type(inner) = arguments.args.first()? else {
4229        return None;
4230    };
4231    Some(inner.clone())
4232}
4233
4234#[cfg(test)]
4235mod tests {
4236    use super::{
4237        Attribute, FieldRules, FieldShape, Fields, ItemEnum, ItemStruct, ModelArgs, NumericLiteral,
4238        ProviderArgs, ProviderLifetimeArgument, RenameRule, Type, constraint_encodings,
4239        enum_model_tokens, field_shape, lint_pattern_syntax, metadata_encodings, model_tokens,
4240        normalize_collection_rules, provider_tokens, quote, reject_incompatible_rules, schema_type,
4241        snake_to_camel, take_field_rules,
4242    };
4243
4244    #[test]
4245    fn a_provider_may_read_request_inputs_and_a_singleton_may_not() {
4246        let request = ProviderArgs {
4247            lifetime: ProviderLifetimeArgument::Request,
4248        };
4249        let function = syn::parse_str::<syn::ItemFn>(
4250            "fn tenant(Header(tenant): Header<String>, repo: Depends<Repo>) -> Tenant { todo!() }",
4251        )
4252        .expect("provider fixture parses");
4253        let expansion = provider_tokens(&request, &function)
4254            .expect("a request provider with inputs expands")
4255            .to_string();
4256        assert!(expansion.contains("RequestProvider"), "{expansion}");
4257        assert!(expansion.contains("request_from_slots"), "{expansion}");
4258        assert!(expansion.contains("InputSource :: Header"), "{expansion}");
4259
4260        let singleton = ProviderArgs {
4261            lifetime: ProviderLifetimeArgument::Singleton,
4262        };
4263        let error = provider_tokens(&singleton, &function)
4264            .expect_err("a singleton is built before any request exists");
4265        assert!(
4266            error.to_string().contains("cannot declare request inputs"),
4267            "{error}"
4268        );
4269
4270        let body = syn::parse_str::<syn::ItemFn>(
4271            "fn bad(Json(payload): Json<Model>) -> Model { todo!() }",
4272        )
4273        .expect("body fixture parses");
4274        let error = provider_tokens(&request, &body).expect_err("a provider must not own the body");
4275        assert!(error.to_string().contains("body payloads"), "{error}");
4276    }
4277
4278    fn field_attributes(declaration: &str) -> Vec<Attribute> {
4279        let model = syn::parse_str::<ItemStruct>(&format!("struct Probe {{ {declaration} }}"))
4280            .expect("fixture struct parses");
4281        let Fields::Named(fields) = model.fields else {
4282            panic!("fixture struct must use named fields");
4283        };
4284        fields
4285            .named
4286            .into_iter()
4287            .next()
4288            .expect("fixture struct declares one field")
4289            .attrs
4290    }
4291
4292    fn rules_of(declaration: &str) -> syn::Result<FieldRules> {
4293        let mut attributes = field_attributes(declaration);
4294        take_field_rules(&mut attributes)
4295    }
4296
4297    fn parse_type(source: &str) -> Type {
4298        syn::parse_str::<Type>(source).expect("fixture type parses")
4299    }
4300
4301    fn rejection_message<T>(result: syn::Result<T>) -> String {
4302        match result {
4303            Ok(_) => panic!("the declaration must be rejected"),
4304            Err(error) => error.to_string(),
4305        }
4306    }
4307
4308    fn reject(declaration: &str, field_type: &str) -> String {
4309        let rules = rules_of(declaration).expect("attributes parse");
4310        let field_type = parse_type(field_type);
4311        let shape = field_shape(&field_type);
4312        reject_incompatible_rules(&rules, &field_type, shape)
4313            .expect_err("the rule must be rejected")
4314            .to_string()
4315    }
4316
4317    fn expand(source: &str, arguments: &ModelArgs) -> syn::Result<String> {
4318        let mut model = syn::parse_str::<ItemStruct>(source).expect("fixture model parses");
4319        model_tokens(arguments, &mut model).map(|tokens| tokens.to_string())
4320    }
4321
4322    fn expand_default(source: &str) -> syn::Result<String> {
4323        expand(source, &ModelArgs::default())
4324    }
4325
4326    #[test]
4327    fn field_shapes_classify_strings_numbers_and_collections() {
4328        assert_eq!(field_shape(&parse_type("String")), FieldShape::Text);
4329        assert_eq!(field_shape(&parse_type("u8")), FieldShape::Integer);
4330        assert_eq!(field_shape(&parse_type("i128")), FieldShape::Integer);
4331        assert_eq!(field_shape(&parse_type("usize")), FieldShape::Integer);
4332        assert_eq!(field_shape(&parse_type("f32")), FieldShape::Float);
4333        assert_eq!(
4334            field_shape(&parse_type("Vec<Address>")),
4335            FieldShape::Collection
4336        );
4337        assert_eq!(field_shape(&parse_type("Uuid")), FieldShape::Other);
4338        assert_eq!(field_shape(&parse_type("bool")), FieldShape::Other);
4339    }
4340
4341    #[test]
4342    fn numeric_rules_are_rejected_on_non_numeric_fields() {
4343        for rule in [
4344            "#[minimum(1)]",
4345            "#[maximum(1)]",
4346            "#[exclusive_minimum(1)]",
4347            "#[exclusive_maximum(1)]",
4348            "#[multiple_of(2)]",
4349        ] {
4350            let message = reject(&format!("{rule} name: String"), "String");
4351            assert!(
4352                message.contains("requires an integer or floating-point field"),
4353                "{rule} produced {message}"
4354            );
4355            assert!(message.contains("String"), "{rule} produced {message}");
4356        }
4357    }
4358
4359    #[test]
4360    fn collection_rules_are_rejected_outside_collections() {
4361        for rule in ["#[min_items(1)]", "#[max_items(1)]", "#[unique_items]"] {
4362            let message = reject(&format!("{rule} name: String"), "String");
4363            assert!(
4364                message.contains("requires a `Vec<T>` or `Option<Vec<T>>` field"),
4365                "{rule} produced {message}"
4366            );
4367        }
4368    }
4369
4370    #[test]
4371    fn string_rules_are_rejected_outside_strings() {
4372        let message = reject("#[pattern(\"^a$\")] count: u32", "u32");
4373        assert!(
4374            message.contains("`pattern` requires a `String`"),
4375            "{message}"
4376        );
4377        let message = reject("#[email] count: u32", "u32");
4378        assert!(message.contains("`email` requires a `String`"), "{message}");
4379    }
4380
4381    #[test]
4382    fn length_rules_accept_strings_and_collections_but_not_numbers() {
4383        let rules = rules_of("#[min_length(2)] tags: Vec<String>").expect("attributes parse");
4384        let collection = parse_type("Vec<String>");
4385        reject_incompatible_rules(&rules, &collection, FieldShape::Collection)
4386            .expect("collections accept length rules");
4387
4388        let message = reject("#[min_length(2)] count: u32", "u32");
4389        assert!(
4390            message.contains("`min_length` requires a `String`, `Option<String>`, or `Vec<T>`"),
4391            "{message}"
4392        );
4393    }
4394
4395    #[test]
4396    fn inverted_and_degenerate_bounds_are_rejected() {
4397        let message = rejection_message(rules_of("#[min_length(5)] #[max_length(2)] name: String"));
4398        assert!(message.contains("`min_length` cannot be greater than `max_length`"));
4399
4400        let message = rejection_message(rules_of(
4401            "#[min_items(5)] #[max_items(2)] tags: Vec<String>",
4402        ));
4403        assert!(message.contains("`min_items` cannot be greater than `max_items`"));
4404
4405        let message = rejection_message(rules_of("#[minimum(5)] #[maximum(2)] count: u32"));
4406        assert!(message.contains("`minimum` cannot be greater than `maximum`"));
4407
4408        let message = rejection_message(rules_of("#[multiple_of(0)] count: u32"));
4409        assert!(message.contains("`multiple_of` cannot be zero"));
4410    }
4411
4412    #[test]
4413    fn numeric_attributes_accept_negative_and_floating_point_literals() {
4414        let rules = rules_of("#[minimum(-3)] #[maximum(2.5)] ratio: f64").expect("bounds parse");
4415        assert_eq!(
4416            rules.minimum.map(|(value, _)| value),
4417            Some(NumericLiteral::Integer(-3))
4418        );
4419        assert_eq!(
4420            rules.maximum.map(|(value, _)| value),
4421            Some(NumericLiteral::Float(2.5))
4422        );
4423
4424        let message = rejection_message(rules_of("#[minimum(\"one\")] count: u32"));
4425        assert!(
4426            message.contains("integer or floating-point literal"),
4427            "{message}"
4428        );
4429    }
4430
4431    #[test]
4432    fn constraints_use_a_canonical_key_value_encoding() {
4433        let rules = rules_of(
4434            "#[minimum(1)] #[maximum(10.0)] #[exclusive_minimum(0)] \
4435             #[exclusive_maximum(11)] #[multiple_of(2)] count: u32",
4436        )
4437        .expect("bounds parse");
4438        assert_eq!(
4439            constraint_encodings(&rules),
4440            [
4441                "minimum=1",
4442                "maximum=10.0",
4443                "exclusive_minimum=0",
4444                "exclusive_maximum=11",
4445                "multiple_of=2"
4446            ]
4447        );
4448
4449        let rules = rules_of("#[pattern(\"^[a-z]+$\")] slug: String").expect("pattern parses");
4450        assert_eq!(constraint_encodings(&rules), ["pattern=^[a-z]+$"]);
4451
4452        let rules = rules_of("#[min_items(1)] #[max_items(4)] #[unique_items] tags: Vec<String>")
4453            .expect("collection rules parse");
4454        assert_eq!(
4455            constraint_encodings(&rules),
4456            ["min_items=1", "max_items=4", "unique_items=true"]
4457        );
4458    }
4459
4460    #[test]
4461    fn collection_length_rules_are_folded_into_item_bounds() {
4462        let mut rules = rules_of("#[min_length(2)] #[max_length(4)] tags: Vec<String>")
4463            .expect("length rules parse");
4464        normalize_collection_rules(&mut rules, FieldShape::Collection);
4465        assert!(rules.min_length.is_none());
4466        assert!(rules.max_length.is_none());
4467        assert_eq!(constraint_encodings(&rules), ["min_items=2", "max_items=4"]);
4468    }
4469
4470    #[test]
4471    fn unsupported_patterns_are_rejected_when_the_macro_expands() {
4472        for (pattern, fragment) in [
4473            ("", "the pattern is empty"),
4474            ("^a{2,3}$", "counted repetition"),
4475            ("^(?:a)$", "not supported"),
4476            ("^(a$", "unbalanced group"),
4477            ("^a)$", "unbalanced group"),
4478            ("^[a-z$", "unterminated character class"),
4479            ("^*a$", "no preceding expression"),
4480            (r"^\q$", "the escape `\\q` is not supported"),
4481            (r"^a\", "lone backslash"),
4482            ("^a$b$", "`$` is supported only at the end"),
4483            ("a^b", "`^` is supported only at the start"),
4484        ] {
4485            let error = lint_pattern_syntax(pattern)
4486                .expect_err(&format!("{pattern} must be rejected"))
4487                .to_string();
4488            assert!(error.contains(fragment), "{pattern} produced {error}");
4489        }
4490    }
4491
4492    #[test]
4493    fn supported_patterns_pass_the_compile_time_lint() {
4494        for pattern in [
4495            "^[a-z][a-z0-9_]*$",
4496            r"^(cat|dog)-\d+$",
4497            r"\w+@\w+\.\w+",
4498            "^a[^0-9]?$",
4499            r"^cost\$",
4500            "^[a-z-]+$",
4501        ] {
4502            assert!(
4503                lint_pattern_syntax(pattern).is_ok(),
4504                "{pattern} must be accepted"
4505            );
4506        }
4507    }
4508
4509    #[test]
4510    fn nested_models_recurse_without_an_explicit_attribute() {
4511        let expansion = expand_default("struct Order { address: Address, items: Vec<Line> }")
4512            .expect("model expands");
4513        assert!(expansion.contains("__blazingly_nested"));
4514        assert!(expansion.contains("__blazingly_nested_items"));
4515        assert!(expansion.contains("__blazingly_is_model"));
4516    }
4517
4518    #[test]
4519    fn explicit_nested_stays_accepted_and_still_marks_the_descriptor() {
4520        let expansion =
4521            expand_default("struct Order { #[nested] address: Address }").expect("model expands");
4522        assert!(expansion.contains("ValidationRule :: Nested"));
4523    }
4524
4525    #[test]
4526    fn scalar_fields_without_rules_emit_no_validation_body() {
4527        let expansion =
4528            expand_default("struct Order { name: String, count: u32 }").expect("model expands");
4529        assert!(!expansion.contains("__BlazinglyValue"));
4530        assert!(!expansion.contains("self . name"));
4531    }
4532
4533    #[test]
4534    fn model_level_validate_with_runs_after_the_field_rules() {
4535        let arguments = ModelArgs {
4536            validator: Some(syn::parse_str("checks::validate_window").expect("path parses")),
4537            ..ModelArgs::default()
4538        };
4539        let expansion = expand(
4540            "struct Window { #[minimum(0)] start: i64, #[minimum(0)] end: i64 }",
4541            &arguments,
4542        )
4543        .expect("model expands");
4544        let validator = expansion
4545            .find("checks :: validate_window")
4546            .expect("the model validator is called");
4547        let last_field_rule = expansion
4548            .rfind("check_minimum")
4549            .expect("field rules are emitted");
4550        assert!(validator > last_field_rule);
4551        assert!(expansion.contains("merge_model_violations"));
4552    }
4553
4554    #[test]
4555    fn model_arguments_reject_unknown_keys() {
4556        let error = rejection_message(syn::parse_str::<ModelArgs>(
4557            "rename_all = \"camelCase\", frobnicate = \"x\"",
4558        ));
4559        assert!(
4560            error.contains("`borrowed`, `rename_all`, and `validate_with`"),
4561            "{error}"
4562        );
4563    }
4564
4565    #[test]
4566    fn snake_case_names_become_camel_case() {
4567        assert_eq!(snake_to_camel("public_name"), "publicName");
4568        assert_eq!(snake_to_camel("id"), "id");
4569        assert_eq!(snake_to_camel("a_b_c"), "aBC");
4570    }
4571
4572    fn borrowed_arguments() -> ModelArgs {
4573        syn::parse_str::<ModelArgs>("borrowed").expect("`borrowed` is a bare flag")
4574    }
4575
4576    fn expand_borrowed(source: &str) -> syn::Result<String> {
4577        expand(source, &borrowed_arguments())
4578    }
4579
4580    #[test]
4581    fn a_borrowed_view_serializes_and_describes_itself_but_never_parses() {
4582        let expansion = expand_borrowed("struct View<'store> { title: &'store str }")
4583            .expect("a borrowed view expands");
4584        assert!(expansion.contains("Serialize"));
4585        assert!(
4586            !expansion.contains("Deserialize"),
4587            "a borrowed view is an output type"
4588        );
4589        assert!(expansion.contains("ApiSchema for View"));
4590        assert!(
4591            !expansion.contains("ApiModel for"),
4592            "a borrowed view is never validated"
4593        );
4594        assert!(!expansion.contains("ValidationErrors"));
4595    }
4596
4597    #[test]
4598    fn borrowed_field_types_document_the_schema_their_owned_form_documents() {
4599        let resolved = |source: &str| {
4600            let ty = schema_type(&parse_type(source));
4601            quote!(#ty).to_string().replace(' ', "")
4602        };
4603        assert_eq!(resolved("&'store str"), "&str");
4604        assert_eq!(resolved("Vec<&'store Tag>"), "Vec<Tag>");
4605        assert_eq!(resolved("Option<&'store str>"), "Option<&str>");
4606        assert_eq!(resolved("&'store [Tag]"), "::std::vec::Vec<Tag>");
4607        assert_eq!(resolved("Cow<'store, str>"), "&str");
4608        assert_eq!(resolved("Page<'store, Tag>"), "Page<Tag>");
4609        // A type that borrows nothing is left exactly as written.
4610        assert_eq!(resolved("Vec<Tag>"), "Vec<Tag>");
4611    }
4612
4613    #[test]
4614    fn a_generic_borrowed_envelope_names_one_schema_per_item_type() {
4615        let expansion = expand_borrowed("struct Page<'store, T> { items: Vec<&'store T> }")
4616            .expect("a generic borrowed view expands");
4617        assert!(expansion.contains("__blazingly_schema_name"));
4618        assert!(expansion.contains("T : :: blazingly :: ApiSchema"));
4619    }
4620
4621    #[test]
4622    fn validation_rules_are_rejected_on_a_borrowed_view() {
4623        let error = rejection_message(expand_borrowed(
4624            "struct View<'store> { #[min_length(2)] title: &'store str }",
4625        ));
4626        assert!(error.contains("`#[min_length]`"), "{error}");
4627        assert!(error.contains("never validated"), "{error}");
4628
4629        let arguments = syn::parse_str::<ModelArgs>("borrowed, validate_with = checks::window")
4630            .expect("parses");
4631        let error = rejection_message(expand("struct View<'a> { title: &'a str }", &arguments));
4632        assert!(error.contains("never validated"), "{error}");
4633    }
4634
4635    fn expand_enum(source: &str, arguments: &ModelArgs) -> syn::Result<String> {
4636        let mut model = syn::parse_str::<ItemEnum>(source).expect("fixture enum parses");
4637        enum_model_tokens(arguments, &mut model).map(|tokens| tokens.to_string())
4638    }
4639
4640    fn model_arguments(source: &str) -> ModelArgs {
4641        syn::parse_str::<ModelArgs>(source).expect("fixture arguments parse")
4642    }
4643
4644    #[test]
4645    fn a_default_is_recorded_as_json_beside_the_field_rules() {
4646        let rules = rules_of("#[default(20)] limit: u32").expect("the default parses");
4647        assert_eq!(metadata_encodings(&rules, false), ["default=20"]);
4648
4649        let rules = rules_of("#[default(-2.5)] ratio: f64").expect("the default parses");
4650        assert_eq!(metadata_encodings(&rules, false), ["default=-2.5"]);
4651
4652        let rules = rules_of("#[default(\"dr\\\"aft\")] status: String").expect("parses");
4653        assert_eq!(metadata_encodings(&rules, false), [r#"default="dr\"aft""#]);
4654
4655        let rules = rules_of("#[default(false)] verbose: bool").expect("the default parses");
4656        assert_eq!(
4657            metadata_encodings(&rules, true),
4658            ["default=false", "nullable=true"]
4659        );
4660    }
4661
4662    #[test]
4663    fn a_default_must_match_the_field_it_fills_in() {
4664        let message = reject("#[default(\"draft\")] limit: u32", "u32");
4665        assert!(
4666            message.contains("a string literal requires a `String` field"),
4667            "{message}"
4668        );
4669
4670        let message = reject("#[default(20)] status: String", "String");
4671        assert!(
4672            message.contains("an integer literal requires an integer or floating-point field"),
4673            "{message}"
4674        );
4675
4676        let message = reject("#[default(true)] limit: u32", "u32");
4677        assert!(
4678            message.contains("a boolean literal requires a `bool` field"),
4679            "{message}"
4680        );
4681
4682        let message = rejection_message(rules_of("#[default(limit())] limit: u32"));
4683        assert!(
4684            message.contains("string, integer, floating-point, or boolean literal"),
4685            "{message}"
4686        );
4687    }
4688
4689    #[test]
4690    fn a_defaulted_field_is_not_optional_and_is_no_longer_required() {
4691        let message = rejection_message(expand_default(
4692            "struct List { #[default(20)] limit: Option<u32> }",
4693        ));
4694        assert!(message.contains("declare it without `Option`"), "{message}");
4695
4696        let expansion =
4697            expand_default("struct List { #[default(20)] limit: u32 }").expect("model expands");
4698        assert!(expansion.contains("__blazingly_default_list_limit"));
4699        assert!(expansion.contains("serde (default = \"__blazingly_default_list_limit\")"));
4700        assert!(
4701            expansion.contains("FieldDescriptor :: new (\"limit\" , false ,"),
4702            "a field with a default is not required of the client: {expansion}"
4703        );
4704    }
4705
4706    #[test]
4707    fn an_optional_field_records_its_nullability() {
4708        let expansion =
4709            expand_default("struct Article { summary: Option<String> }").expect("model expands");
4710        assert!(expansion.contains("\"nullable=true\""));
4711
4712        let expansion = expand_default("struct Article { summary: String }").expect("expands");
4713        assert!(!expansion.contains("nullable"));
4714    }
4715
4716    #[test]
4717    fn a_value_type_declares_one_reusable_bundle_of_rules() {
4718        let expansion = expand_default("struct Title (String) ;").expect("a value type expands");
4719        assert!(expansion.contains("serde (transparent)"));
4720        assert!(expansion.contains("impl :: blazingly :: ApiConstrained for Title"));
4721        assert!(expansion.contains("fn into_inner"));
4722
4723        let mut model = syn::parse_str::<ItemStruct>("#[min_length(8)] struct Title (String) ;")
4724            .expect("parses");
4725        let expansion = model_tokens(&ModelArgs::default(), &mut model)
4726            .expect("a value type with rules expands")
4727            .to_string();
4728        assert!(expansion.contains("ValidationRule :: MinLength (8usize)"));
4729        assert!(expansion.contains("min_length"));
4730    }
4731
4732    #[test]
4733    fn a_value_type_rejects_what_only_a_field_can_carry() {
4734        let message = rejection_message(expand_default("struct Pair (String , u32) ;"));
4735        assert!(message.contains("wraps exactly one field"), "{message}");
4736
4737        let message = rejection_message(expand_default("#[alias(\"t\")] struct Title (String) ;"));
4738        assert!(
4739            message.contains("`alias` names an extra wire key"),
4740            "{message}"
4741        );
4742
4743        let message =
4744            rejection_message(expand_default("#[default(\"x\")] struct Title (String) ;"));
4745        assert!(message.contains("belongs to the field"), "{message}");
4746
4747        let message = rejection_message(expand_default("struct Title (Option<String>) ;"));
4748        assert!(
4749            message.contains("declare the field that uses it"),
4750            "{message}"
4751        );
4752
4753        let message = rejection_message(expand(
4754            "struct Title (String) ;",
4755            &model_arguments("rename_all = \"camelCase\""),
4756        ));
4757        assert!(message.contains("`rename_all` renames fields"), "{message}");
4758    }
4759
4760    #[test]
4761    fn an_enumeration_pins_every_variant_to_an_explicit_wire_value() {
4762        let expansion = expand_enum(
4763            "enum Language { Uk, Ru, En }",
4764            &model_arguments("rename_all = \"lowercase\""),
4765        )
4766        .expect("an enumeration expands");
4767        assert!(expansion.contains("serde (rename = \"uk\")"));
4768        assert!(expansion.contains("\"enum=uk|ru|en\""));
4769        assert!(expansion.contains("SchemaKind :: String"));
4770        assert!(expansion.contains("const VARIANTS"));
4771
4772        let expansion = expand_enum(
4773            "enum Status { NotFound, #[rename(\"ok\")] Fine }",
4774            &ModelArgs::default(),
4775        )
4776        .expect("an enumeration expands");
4777        assert!(expansion.contains("serde (rename = \"NotFound\")"));
4778        assert!(expansion.contains("serde (rename = \"ok\")"));
4779        assert!(expansion.contains("\"enum=NotFound|ok\""));
4780    }
4781
4782    #[test]
4783    fn enum_rename_rules_match_the_serde_spelling() {
4784        let cases = [
4785            ("PascalCase", "NotFound"),
4786            ("lowercase", "notfound"),
4787            ("UPPERCASE", "NOTFOUND"),
4788            ("camelCase", "notFound"),
4789            ("snake_case", "not_found"),
4790            ("SCREAMING_SNAKE_CASE", "NOT_FOUND"),
4791            ("kebab-case", "not-found"),
4792            ("SCREAMING-KEBAB-CASE", "NOT-FOUND"),
4793        ];
4794        for (rule, expected) in cases {
4795            let rule = RenameRule::parse(rule).expect("the rule is supported");
4796            assert_eq!(rule.apply("NotFound"), expected);
4797        }
4798        assert!(RenameRule::parse("Train-Case").is_none());
4799    }
4800
4801    #[test]
4802    fn an_enumeration_rejects_data_carrying_and_ambiguous_variants() {
4803        let message = rejection_message(expand_enum(
4804            "enum Payload { Text(String) }",
4805            &ModelArgs::default(),
4806        ));
4807        assert!(message.contains("a variant cannot carry data"), "{message}");
4808
4809        let message = rejection_message(expand_enum(
4810            "enum Language { Uk, #[rename(\"Uk\")] Ukrainian }",
4811            &ModelArgs::default(),
4812        ));
4813        assert!(message.contains("declared twice"), "{message}");
4814
4815        let message = rejection_message(expand_enum(
4816            "enum Language { #[rename(\"a|b\")] Both }",
4817            &ModelArgs::default(),
4818        ));
4819        assert!(message.contains("`|` separates the variants"), "{message}");
4820
4821        let message = rejection_message(expand_enum("enum Empty { }", &ModelArgs::default()));
4822        assert!(message.contains("at least one variant"), "{message}");
4823
4824        let message = rejection_message(expand_enum(
4825            "enum Language { Uk }",
4826            &model_arguments("rename_all = \"Train-Case\""),
4827        ));
4828        assert!(message.contains("SCREAMING-KEBAB-CASE"), "{message}");
4829    }
4830
4831    #[test]
4832    fn a_field_validator_reports_through_the_undoubled_merge() {
4833        let expansion = expand_default("struct Window { #[validate_with(checks::at)] at: u32 }")
4834            .expect("model expands");
4835        assert!(expansion.contains("merge_field_validation_errors"));
4836        assert!(!expansion.contains("merge_validation_errors (& mut errors"));
4837    }
4838
4839    #[test]
4840    fn an_owning_model_rejects_generics_and_points_at_the_borrowed_form() {
4841        let error = rejection_message(expand_default("struct Page<T> { items: Vec<T> }"));
4842        assert!(error.contains("silently skip validating it"), "{error}");
4843        assert!(error.contains("#[api_model(borrowed)]"), "{error}");
4844
4845        let error = rejection_message(expand_default("struct View<'a> { title: &'a str }"));
4846        assert!(error.contains("cannot borrow from the request"), "{error}");
4847        assert!(error.contains("#[api_model(borrowed)]"), "{error}");
4848    }
4849}