Skip to main content

openapi_to_rust/
client_generator.rs

1//! HTTP client generation for OpenAPI specifications.
2//!
3//! This module is part of the code generator that creates production-ready HTTP clients
4//! from OpenAPI specifications. It generates clients with middleware support including
5//! retry logic and request tracing.
6//!
7//! # Overview
8//!
9//! The client generator creates:
10//! - `HttpClient` struct with middleware stack (reqwest-middleware)
11//! - Retry logic with exponential backoff (reqwest-retry)
12//! - Request/response tracing (reqwest-tracing)
13//! - Direct methods for all API operations (GET, POST, PUT, DELETE, PATCH)
14//! - Comprehensive error handling with [`HttpError`](crate::http_error::HttpError)
15//! - Builder pattern for configuration
16//!
17//! # Generated Code Structure
18//!
19//! For each OpenAPI specification, the generator creates:
20//!
21//! ```rust,ignore
22//! // Generated client.rs file
23//!
24//! use crate::types::*;
25//! use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
26//! use std::collections::BTreeMap;
27//!
28//! pub struct HttpClient {
29//!     base_url: String,
30//!     api_key: Option<String>,
31//!     http_client: ClientWithMiddleware,
32//!     custom_headers: BTreeMap<String, String>,
33//! }
34//!
35//! impl HttpClient {
36//!     pub fn new() -> Self { /* ... */ }
37//!     pub fn with_client(client: reqwest::Client) -> Self { /* ... */ }
38//!     pub fn with_config(retry_config: Option<RetryConfig>, enable_tracing: bool) -> Self { /* ... */ }
39//!     pub fn with_base_url(self, base_url: String) -> Self { /* ... */ }
40//!     pub fn with_api_key(self, api_key: String) -> Self { /* ... */ }
41//!     pub fn with_header(self, key: String, value: String) -> Self { /* ... */ }
42//!
43//!     // Generated operation methods
44//!     pub async fn list_items(&self) -> Result<ItemList, HttpError> { /* ... */ }
45//!     pub async fn create_item(&self, request: CreateItemRequest) -> Result<Item, HttpError> { /* ... */ }
46//!     pub async fn get_item(&self, id: impl AsRef<str>) -> Result<Item, HttpError> { /* ... */ }
47//! }
48//! ```
49//!
50//! # Middleware Stack
51//!
52//! The generated client uses `reqwest-middleware` to build a composable middleware stack:
53//!
54//! 1. **Tracing Middleware** (optional, enabled by default)
55//!    - Logs HTTP requests/responses
56//!    - Creates spans for distributed tracing
57//!    - Integrates with `tracing` ecosystem
58//!
59//! 2. **Retry Middleware** (optional, configured via TOML)
60//!    - Exponential backoff retry policy
61//!    - Automatically retries transient errors (429, 500, 502, 503, 504)
62//!    - Configurable max retries and delay bounds
63//!
64//! # Configuration
65//!
66//! ## Via TOML
67//!
68//! ```toml
69//! [http_client]
70//! base_url = "https://api.example.com"
71//! timeout_seconds = 30
72//!
73//! [http_client.retry]
74//! max_retries = 3
75//! initial_delay_ms = 500
76//! max_delay_ms = 16000
77//!
78//! [http_client.tracing]
79//! enabled = true
80//! ```
81//!
82//! ## Via Rust API
83//!
84//! ```no_run
85//! use openapi_to_rust::{GeneratorConfig, http_config::*};
86//! use std::path::PathBuf;
87//!
88//! let config = GeneratorConfig {
89//!     spec_path: PathBuf::from("openapi.json"),
90//!     enable_async_client: true,
91//!     retry_config: Some(RetryConfig {
92//!         max_retries: 3,
93//!         initial_delay_ms: 500,
94//!         max_delay_ms: 16000,
95//!     }),
96//!     tracing_enabled: true,
97//!     // ... other fields
98//!     ..Default::default()
99//! };
100//! ```
101//!
102//! # Generated Client Usage
103//!
104//! ```rust,ignore
105//! use crate::generated::client::HttpClient;
106//!
107//! #[tokio::main]
108//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
109//!     // Create client with retry and tracing
110//!     let client = HttpClient::new()
111//!         .with_base_url("https://api.example.com".to_string())
112//!         .with_api_key("your-api-key".to_string())
113//!         .with_header("X-Custom-Header".to_string(), "value".to_string());
114//!
115//!     // Make API calls - retries happen automatically
116//!     let items = client.list_items().await?;
117//!     println!("Found {} items", items.items.len());
118//!
119//!     Ok(())
120//! }
121//! ```
122//!
123//! # HTTP Method Support
124//!
125//! The generator supports all standard HTTP methods:
126//! - `GET` - List and retrieve operations
127//! - `POST` - Create operations
128//! - `PUT` - Full update operations
129//! - `PATCH` - Partial update operations
130//! - `DELETE` - Delete operations
131//!
132//! # Error Handling
133//!
134//! All generated methods return `Result<T, HttpError>` where `HttpError` provides:
135//! - Detailed error information
136//! - Retry detection via `is_retryable()`
137//! - Error categorization (client errors, server errors)
138//!
139//! See [`http_error`](crate::http_error) module for details.
140//!
141//! # Implementation Details
142//!
143//! The generator uses the following approach:
144//! 1. Analyzes OpenAPI operations to extract HTTP methods, paths, parameters
145//! 2. Generates typed request/response handling
146//! 3. Creates method signatures with proper parameter types
147//! 4. Generates path parameter substitution
148//! 5. Handles query parameters and request bodies
149//! 6. Configures middleware stack based on generator config
150
151use crate::analysis::{
152    OperationInfo, OperationRepresentation, OperationResponseBody, ParameterInfo, SchemaAnalysis,
153};
154use crate::generator::CodeGenerator;
155use heck::{ToPascalCase, ToSnakeCase};
156use proc_macro2::TokenStream;
157use quote::{format_ident, quote};
158use std::collections::BTreeMap;
159
160struct AllocatedOperationParam<'a> {
161    param: &'a ParameterInfo,
162    ident: syn::Ident,
163}
164
165#[derive(Clone)]
166struct BodyFieldPlan {
167    wire_name: String,
168    preferred_method_name: String,
169    value_ident: syn::Ident,
170    value_type: TokenStream,
171    access_path: Vec<syn::Ident>,
172    tri_state: bool,
173}
174
175#[derive(Clone, Copy)]
176enum MultipartClientFieldKind {
177    RawBytes,
178    Base64,
179    Base64UrlUnpadded,
180    Text,
181    /// An object, or a union of them, sent as one JSON part.
182    Json,
183}
184
185#[derive(Clone, Copy)]
186enum ClientSuccessBody<'a> {
187    Json(&'a str),
188    Text,
189    Binary,
190    EventStream,
191    ParsedSse,
192    Empty,
193}
194
195#[derive(Clone)]
196struct ClientSuccessSelection<'a> {
197    statuses: Vec<&'a str>,
198    excluded_statuses: Vec<&'a str>,
199    body: ClientSuccessBody<'a>,
200    accept: Option<&'a str>,
201}
202
203/// Final flat client binding shared by rendering, builders and metadata.
204pub(crate) struct ClientMethodPlan<'a> {
205    pub operation: &'a OperationInfo,
206    pub method_ident: syn::Ident,
207    pub request_parameters: TokenStream,
208    pub generics: TokenStream,
209    pub response_type: TokenStream,
210    pub error_type: TokenStream,
211    pub response: ClientResponsePlan,
212    pub builder_method_ident: Option<syn::Ident>,
213    pub builder_type_ident: Option<syn::Ident>,
214    success: ClientSuccessSelection<'a>,
215    validate_content_type: bool,
216}
217
218pub(crate) struct ClientResponsePlan {
219    pub statuses: Vec<String>,
220    pub excluded_statuses: Vec<String>,
221    pub media_type: Option<String>,
222    pub kind: String,
223    pub consumption: String,
224}
225
226enum RequiredBodyConstruction {
227    Default,
228    New(Vec<BodyConstructorParam>),
229    Whole,
230}
231
232struct BodyConstructorParam {
233    preferred_ident: syn::Ident,
234    value_type: TokenStream,
235}
236
237struct BodyModelPlan {
238    body_ident: syn::Ident,
239    body_type: TokenStream,
240    required_construction: RequiredBodyConstruction,
241    optional_fields: Vec<BodyFieldPlan>,
242}
243
244impl CodeGenerator {
245    /// Generate the HTTP client struct with middleware support
246    pub fn generate_http_client_struct(&self) -> TokenStream {
247        let has_retry = self.config().retry_config.is_some();
248        let has_tracing = self.config().tracing_enabled;
249
250        // Generate RetryConfig struct if needed
251        let retry_config_struct = if has_retry {
252            quote! {
253                /// Retry configuration for HTTP requests
254                #[derive(Debug, Clone)]
255                pub struct RetryConfig {
256                    pub max_retries: u32,
257                    pub initial_delay_ms: u64,
258                    pub max_delay_ms: u64,
259                }
260
261                impl Default for RetryConfig {
262                    fn default() -> Self {
263                        Self {
264                            max_retries: 3,
265                            initial_delay_ms: 500,
266                            max_delay_ms: 16000,
267                        }
268                    }
269                }
270            }
271        } else {
272            quote! {}
273        };
274
275        // Generate the main HttpClient struct
276        let client_struct = quote! {
277            use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
278            use std::collections::BTreeMap;
279
280            /// Default upper bound for any response body buffered in memory.
281            pub const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 8 * 1024 * 1024;
282
283            /// HTTP client for making API requests
284            #[derive(Clone)]
285            pub struct HttpClient {
286                base_url: String,
287                api_key: Option<String>,
288                http_client: ClientWithMiddleware,
289                custom_headers: BTreeMap<String, String>,
290                max_response_body_bytes: usize,
291            }
292
293            // Buffer a response body while enforcing `limit`, using
294            // `bytes_stream()` rather than `Response::chunk()`: `chunk()` is
295            // native-only in reqwest and does not exist on the wasm32 backend,
296            // so it broke every generated client under `trunk serve`
297            // (openapi-generator-xhz, issue #74). `bytes_stream()` is available
298            // on both targets behind reqwest's `stream` feature.
299            async fn __read_bounded_response_body(
300                response: reqwest::Response,
301                limit: usize,
302            ) -> Result<Vec<u8>, HttpError> {
303                use futures_util::StreamExt;
304
305                let mut body = Vec::new();
306                let mut stream = std::pin::pin!(response.bytes_stream());
307                while let Some(chunk) = stream.next().await {
308                    let chunk = chunk.map_err(HttpError::Network)?;
309                    let next_len = body.len().checked_add(chunk.len());
310                    if next_len.is_none_or(|next_len| next_len > limit) {
311                        return Err(HttpError::ResponseTooLarge { limit });
312                    }
313                    body.extend_from_slice(&chunk);
314                }
315                Ok(body)
316            }
317        };
318
319        // Generate constructor
320        let constructor = self.generate_constructor(has_retry, has_tracing);
321
322        // Generate builder methods
323        let builder_methods = self.generate_builder_methods();
324
325        // Generate Default implementation
326        let default_impl = quote! {
327            impl Default for HttpClient {
328                fn default() -> Self {
329                    Self::new()
330                }
331            }
332        };
333
334        // Path-segment percent encoder, used by url construction (T5).
335        // Encodes per RFC3986 §3.3: only ALPHA, DIGIT, and `-._~` pass through;
336        // everything else becomes `%XX`.
337        let path_encoder = quote! {
338            fn __pct_encode_path_segment(s: &str) -> String {
339                let mut out = String::with_capacity(s.len());
340                for &b in s.as_bytes() {
341                    match b {
342                        b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
343                            out.push(b as char);
344                        }
345                        _ => {
346                            out.push('%');
347                            out.push_str(&format!("{:02X}", b));
348                        }
349                    }
350                }
351                out
352            }
353
354        };
355
356        // Combine all parts
357        quote! {
358            #retry_config_struct
359            #client_struct
360
361            impl HttpClient {
362                #constructor
363                #builder_methods
364            }
365
366            #default_impl
367            #path_encoder
368        }
369    }
370
371    /// Generate the constructor method
372    fn generate_constructor(&self, has_retry: bool, has_tracing: bool) -> TokenStream {
373        // Seed `base_url` from configuration rather than always starting empty.
374        // A client built with an empty base URL sends every request to a
375        // relative path and fails, so a user who set `[http_client] base_url`
376        // in their TOML — or whose spec declares `servers[0].url`, which the
377        // config layer resolves into the same field — previously had to repeat
378        // it via `with_base_url` or watch every call 404 (openapi-generator-igg).
379        let configured_base_url = self
380            .config()
381            .http_client_config
382            .as_ref()
383            .and_then(|http| http.base_url.as_deref())
384            .unwrap_or_default();
385        let default_base_url = quote! { #configured_base_url.to_string() };
386        let max_response_body_bytes = self
387            .config()
388            .http_client_config
389            .as_ref()
390            .and_then(|http| http.max_response_body_bytes)
391            .unwrap_or(8 * 1024 * 1024);
392
393        let retry_param = if has_retry {
394            quote! { retry_config: Option<RetryConfig>, }
395        } else {
396            quote! {}
397        };
398
399        let tracing_param = if has_tracing {
400            quote! { enable_tracing: bool, }
401        } else {
402            quote! {}
403        };
404
405        let retry_middleware = if has_retry {
406            quote! {
407                if let Some(config) = retry_config {
408                    use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
409
410                    let retry_policy = ExponentialBackoff::builder()
411                        .retry_bounds(
412                            std::time::Duration::from_millis(config.initial_delay_ms),
413                            std::time::Duration::from_millis(config.max_delay_ms),
414                        )
415                        .build_with_max_retries(config.max_retries);
416
417                    let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
418                    client_builder = client_builder.with(retry_middleware);
419                }
420            }
421        } else {
422            quote! {}
423        };
424
425        let tracing_middleware = if has_tracing {
426            quote! {
427                if enable_tracing {
428                    use reqwest_tracing::TracingMiddleware;
429                    client_builder = client_builder.with(TracingMiddleware::default());
430                }
431            }
432        } else {
433            quote! {}
434        };
435
436        let default_constructor = if has_retry && has_tracing {
437            quote! {
438                /// Create a new HTTP client with default configuration
439                pub fn new() -> Self {
440                    Self::with_config(None, true)
441                }
442            }
443        } else if has_retry {
444            quote! {
445                /// Create a new HTTP client with default configuration
446                pub fn new() -> Self {
447                    Self::with_config(None)
448                }
449            }
450        } else if has_tracing {
451            quote! {
452                /// Create a new HTTP client with default configuration
453                pub fn new() -> Self {
454                    Self::with_config(true)
455                }
456            }
457        } else {
458            quote! {
459                /// Create a new HTTP client with default configuration
460                pub fn new() -> Self {
461                    Self::with_client(reqwest::Client::new())
462                }
463            }
464        };
465
466        if has_retry || has_tracing {
467            let retry_arg = has_retry.then(|| quote! { retry_config, });
468            let tracing_arg = has_tracing.then(|| quote! { enable_tracing, });
469            let default_retry_arg = has_retry.then(|| quote! { None, });
470            let default_tracing_arg = has_tracing.then(|| quote! { true, });
471
472            quote! {
473                #default_constructor
474
475                /// Create a new HTTP client with custom configuration
476                pub fn with_config(#retry_param #tracing_param) -> Self {
477                    Self::with_client_and_config(reqwest::Client::new(), #retry_arg #tracing_arg)
478                }
479
480                /// Create an HTTP client using a preconfigured reqwest client.
481                ///
482                /// Preserves the supplied client's transport settings (for example,
483                /// TLS identity, trusted roots, proxy, timeout, and default headers).
484                /// Uses the same middleware defaults as `new()`.
485                pub fn with_client(client: reqwest::Client) -> Self {
486                    Self::with_client_and_config(client, #default_retry_arg #default_tracing_arg)
487                }
488
489                /// Create an HTTP client using a preconfigured reqwest client
490                /// and explicit retry/tracing configuration.
491                ///
492                /// Wraps the supplied client with the generated middleware stack.
493                pub fn with_client_and_config(client: reqwest::Client, #retry_param #tracing_param) -> Self {
494                    let mut client_builder = ClientBuilder::new(client);
495
496                    #tracing_middleware
497                    #retry_middleware
498
499                    let http_client = client_builder.build();
500
501                    Self {
502                        base_url: #default_base_url,
503                        api_key: None,
504                        http_client,
505                        custom_headers: BTreeMap::new(),
506                        max_response_body_bytes: #max_response_body_bytes,
507                    }
508                }
509            }
510        } else {
511            quote! {
512                #default_constructor
513
514                /// Create an HTTP client using a preconfigured reqwest client.
515                ///
516                /// Preserves the supplied client's transport settings (for example,
517                /// TLS identity, trusted roots, proxy, timeout, and default headers).
518                pub fn with_client(client: reqwest::Client) -> Self {
519                    let http_client = ClientBuilder::new(client).build();
520
521                    Self {
522                        base_url: #default_base_url,
523                        api_key: None,
524                        http_client,
525                        custom_headers: BTreeMap::new(),
526                        max_response_body_bytes: #max_response_body_bytes,
527                    }
528                }
529            }
530        }
531    }
532
533    /// Generate builder methods for configuration
534    fn generate_builder_methods(&self) -> TokenStream {
535        quote! {
536            /// Set the base URL for all requests
537            pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
538                self.base_url = base_url.into();
539                self
540            }
541
542            /// Set the API key for authentication
543            pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
544                self.api_key = Some(api_key.into());
545                self
546            }
547
548            /// Set the maximum number of response-body bytes buffered in memory.
549            pub fn with_max_response_body_bytes(mut self, limit: usize) -> Self {
550                self.max_response_body_bytes = limit;
551                self
552            }
553
554            /// Add a custom header to all requests
555            pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
556                self.custom_headers.insert(name.into(), value.into());
557                self
558            }
559
560            /// Add multiple custom headers
561            pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
562                self.custom_headers.extend(headers);
563                self
564            }
565        }
566    }
567
568    /// Generate HTTP operation methods for the client.
569    ///
570    /// Emits per-operation typed error enums (one variant per declared non-2xx
571    /// response with a body schema) BEFORE the `impl HttpClient` block so the
572    /// generated method signatures can reference them. This low-level helper
573    /// also emits operation-owned helpers such as the reserved path encoder.
574    /// intentionally emits every analyzed operation; use
575    /// [`Self::generate_http_client`] or [`Self::generate_all`] to honor the
576    /// configured `[client].operations` scope.
577    /// Generate standalone HTTP methods. Declared SSE alternatives return raw
578    /// byte streams; `generate_all` bundles the optional parsed SSE runtime.
579    pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
580        let operations: Vec<&OperationInfo> = analysis.operations.values().collect();
581        let mut config = self.config().clone();
582        config.enable_sse_client = false;
583        CodeGenerator::new(config).generate_operation_methods_for(analysis, &operations)
584    }
585
586    /// RFC 6570 reserved-expansion encoder, emitted only when an operation has a
587    /// path parameter with `allowReserved: true`.
588    fn generate_reserved_path_encoder(
589        &self,
590        operations: &[&crate::analysis::OperationInfo],
591    ) -> TokenStream {
592        let used = operations.iter().any(|op| {
593            op.parameters
594                .iter()
595                .any(|p| p.location == "path" && p.allow_reserved)
596        });
597        if !used {
598            return TokenStream::new();
599        }
600        quote! {
601            // RFC 6570 reserved expansion for path parameters with
602            // `allowReserved: true`: unreserved and reserved bytes and existing
603            // `%XX` triples pass through, except `/`, `?` and `#`, which OpenAPI
604            // path templating forbids unescaped in a parameter value.
605            fn __pct_encode_path_reserved(s: &str) -> String {
606                let bytes = s.as_bytes();
607                let mut out = String::with_capacity(bytes.len());
608                let mut i = 0;
609                while i < bytes.len() {
610                    let b = bytes[i];
611                    match b {
612                        b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
613                        | b':' | b'@' | b'[' | b']' | b'!' | b'$' | b'&' | b'\'' | b'('
614                        | b')' | b'*' | b'+' | b',' | b';' | b'=' => out.push(b as char),
615                        b'%' if i + 2 < bytes.len()
616                            && bytes[i + 1].is_ascii_hexdigit()
617                            && bytes[i + 2].is_ascii_hexdigit() =>
618                        {
619                            out.push_str(&s[i..i + 3]);
620                            i += 2;
621                        }
622                        _ => {
623                            out.push('%');
624                            out.push_str(&format!("{:02X}", b));
625                        }
626                    }
627                    i += 1;
628                }
629                out
630            }
631        }
632    }
633
634    /// Generate every operation-owned client artifact from one resolved
635    /// operation slice, including any required path encoder. This keeps helpers,
636    /// methods, parameter enums, and typed errors in lockstep for selective clients.
637    pub(crate) fn generate_operation_methods_for(
638        &self,
639        analysis: &SchemaAnalysis,
640        operations: &[&OperationInfo],
641    ) -> TokenStream {
642        let param_enums = self.generate_param_enum_types(operations);
643        let reserved_path_encoder = self.generate_reserved_path_encoder(operations);
644
645        let op_error_enums: Vec<TokenStream> = operations
646            .iter()
647            .copied()
648            .filter_map(|op| self.generate_op_error_enum(op))
649            .collect();
650
651        let plans = self.client_method_plans(analysis, operations);
652        let methods: Vec<TokenStream> = plans
653            .iter()
654            .map(|plan| self.generate_planned_operation_method(analysis, plan))
655            .collect();
656        let (operation_builders, builder_entries) =
657            self.generate_operation_builders(analysis, &plans);
658
659        quote! {
660            #reserved_path_encoder
661
662            #param_enums
663
664            #(#op_error_enums)*
665
666            #(#operation_builders)*
667
668            impl HttpClient {
669                #(#methods)*
670                #(#builder_entries)*
671            }
672        }
673    }
674
675    pub(crate) fn client_method_plans<'a>(
676        &self,
677        analysis: &'a SchemaAnalysis,
678        operations: &[&'a OperationInfo],
679    ) -> Vec<ClientMethodPlan<'a>> {
680        // Reserve every real operation before allocating additive bindings.
681        let mut used: std::collections::HashSet<String> = [
682            "new",
683            "with_config",
684            "with_client",
685            "with_client_and_config",
686            "with_base_url",
687            "with_api_key",
688            "with_max_response_body_bytes",
689            "with_header",
690            "with_headers",
691        ]
692        .into_iter()
693        .map(str::to_owned)
694        .collect();
695        let inherent = used.clone();
696        for operation in operations {
697            let ident = self.get_method_name(operation).to_string();
698            used.insert(ident.strip_prefix("r#").unwrap_or(&ident).to_string());
699        }
700        let mut assigned = std::collections::HashSet::new();
701        let mut base_names = BTreeMap::new();
702        for operation in operations {
703            let ident = self.get_method_name(operation).to_string();
704            let plain = ident.strip_prefix("r#").unwrap_or(&ident);
705            let name = if inherent.contains(plain) || !assigned.insert(plain.to_string()) {
706                Self::allocate_name(&ident, &mut used)
707            } else {
708                ident
709            };
710            base_names.insert(operation.operation_id.as_str(), Self::to_field_ident(&name));
711        }
712        let mut plans = Vec::new();
713        for &operation in operations {
714            let method_ident = base_names[operation.operation_id.as_str()].clone();
715            let success = self.get_success_response(analysis, operation);
716            plans.push(self.make_client_method_plan(
717                operation,
718                method_ident.clone(),
719                success.clone(),
720                "buffered",
721                false,
722            ));
723            let Some(responses) = analysis.operation_responses.get(&operation.operation_id) else {
724                continue;
725            };
726            let mut representations: BTreeMap<&str, Vec<(&str, &OperationRepresentation)>> =
727                BTreeMap::new();
728            for (status, response) in responses
729                .iter()
730                .filter(|(status, _)| status.starts_with('2'))
731            {
732                for (media_type, representation) in &response.representations {
733                    representations
734                        .entry(media_type)
735                        .or_default()
736                        .push((status, representation));
737                }
738            }
739            let mut kind_counts = BTreeMap::new();
740            for alternatives in representations.values() {
741                let kind = Self::representation_kind(alternatives[0].1);
742                *kind_counts.entry(kind).or_insert(0usize) += 1;
743            }
744            for (media_type, alternatives) in representations {
745                let representation = alternatives[0].1;
746                let incompatible = alternatives
747                    .iter()
748                    .any(|(_, other)| *other != representation);
749                let groups: Vec<Vec<(&str, &OperationRepresentation)>> = if incompatible {
750                    alternatives
751                        .into_iter()
752                        .map(|alternative| vec![alternative])
753                        .collect()
754                } else {
755                    vec![alternatives]
756                };
757                for alternatives in groups {
758                    let representation = alternatives[0].1;
759                    let kind = Self::representation_kind(representation);
760                    let suffix = if kind_counts[kind] > 1 {
761                        format!("{kind}_{}", media_type.to_snake_case())
762                    } else {
763                        kind.to_string()
764                    };
765                    let suffix = if incompatible {
766                        format!("{suffix}_status_{}", alternatives[0].0.to_ascii_lowercase())
767                    } else {
768                        suffix
769                    };
770                    let body = match representation {
771                        OperationRepresentation::Json { schema_name } => {
772                            ClientSuccessBody::Json(schema_name)
773                        }
774                        OperationRepresentation::Text => ClientSuccessBody::Text,
775                        OperationRepresentation::Binary { .. } => ClientSuccessBody::Binary,
776                        OperationRepresentation::EventStream => ClientSuccessBody::EventStream,
777                    };
778                    let selection = ClientSuccessSelection {
779                        statuses: alternatives.iter().map(|(status, _)| *status).collect(),
780                        excluded_statuses: responses
781                            .keys()
782                            .filter(|status| {
783                                status.len() == 3
784                                    && status.bytes().all(|c| c.is_ascii_digit())
785                                    && !alternatives
786                                        .iter()
787                                        .any(|(selected, _)| selected == &status.as_str())
788                            })
789                            .map(String::as_str)
790                            .collect(),
791                        body,
792                        accept: Some(media_type),
793                    };
794                    let default = self.get_success_response(analysis, operation);
795                    let equivalent =
796                        Self::success_bodies_are_compatible(default.body, selection.body)
797                            && default.statuses == selection.statuses
798                            && default.accept == selection.accept;
799                    if !matches!(representation, OperationRepresentation::EventStream)
800                        && !equivalent
801                    {
802                        let name =
803                            Self::allocate_name(&format!("{method_ident}_{suffix}"), &mut used);
804                        plans.push(self.make_client_method_plan(
805                            operation,
806                            Self::to_field_ident(&name),
807                            selection.clone(),
808                            "buffered",
809                            true,
810                        ));
811                    }
812                    if matches!(representation, OperationRepresentation::Binary { .. }) {
813                        let name = Self::allocate_name(
814                            &format!("{method_ident}_{suffix}_stream"),
815                            &mut used,
816                        );
817                        let live_selection = ClientSuccessSelection {
818                            body: ClientSuccessBody::EventStream,
819                            ..selection.clone()
820                        };
821                        let mut plan = self.make_client_method_plan(
822                            operation,
823                            Self::to_field_ident(&name),
824                            live_selection,
825                            "binary_stream",
826                            true,
827                        );
828                        plan.response.kind = "binary".to_string();
829                        plans.push(plan);
830                    }
831                    if matches!(representation, OperationRepresentation::EventStream) {
832                        if self.config().enable_sse_client {
833                            let name =
834                                Self::allocate_name(&format!("{method_ident}_sse"), &mut used);
835                            let selection = ClientSuccessSelection {
836                                body: ClientSuccessBody::ParsedSse,
837                                ..selection
838                            };
839                            plans.push(self.make_client_method_plan(
840                                operation,
841                                Self::to_field_ident(&name),
842                                selection,
843                                "parsed_sse",
844                                true,
845                            ));
846                        } else if !equivalent {
847                            let name =
848                                Self::allocate_name(&format!("{method_ident}_{suffix}"), &mut used);
849                            plans.push(self.make_client_method_plan(
850                                operation,
851                                Self::to_field_ident(&name),
852                                selection,
853                                "raw_event_stream",
854                                true,
855                            ));
856                        }
857                    }
858                }
859            }
860        }
861        self.plan_operation_builders(analysis, &mut plans);
862        plans
863    }
864
865    fn representation_kind(representation: &OperationRepresentation) -> &'static str {
866        match representation {
867            OperationRepresentation::Json { .. } => "json",
868            OperationRepresentation::Text => "text",
869            OperationRepresentation::Binary { .. } => "binary",
870            OperationRepresentation::EventStream => "event_stream",
871        }
872    }
873
874    fn make_client_method_plan<'a>(
875        &self,
876        operation: &'a OperationInfo,
877        method_ident: syn::Ident,
878        success: ClientSuccessSelection<'a>,
879        consumption: &str,
880        validate_content_type: bool,
881    ) -> ClientMethodPlan<'a> {
882        let precise_stream = matches!(success.body, ClientSuccessBody::EventStream);
883        let mut captures = Vec::new();
884        let request = if precise_stream {
885            self.generate_request_param_with_capture(operation, Some(&mut captures))
886        } else {
887            self.generate_request_param(operation)
888        };
889        let generics = if captures.is_empty() {
890            TokenStream::new()
891        } else {
892            quote! { <#(#captures: AsRef<str>),*> }
893        };
894        let request_parameters = request;
895        let response_type = if precise_stream {
896            quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + 'static + use<#(#captures),*> }
897        } else if consumption == "parsed_sse" {
898            quote! { super::sse::BoxSseStream<Result<super::sse::SseEvent<String>, super::sse::StreamingError>> }
899        } else {
900            self.response_type_for_body(success.body)
901        };
902        let kind = match success.body {
903            ClientSuccessBody::Json(_) => "json",
904            ClientSuccessBody::Text => "text",
905            ClientSuccessBody::Binary => "binary",
906            ClientSuccessBody::EventStream | ClientSuccessBody::ParsedSse => "event_stream",
907            ClientSuccessBody::Empty => "empty",
908        };
909        let consumption = if matches!(success.body, ClientSuccessBody::EventStream)
910            && consumption == "buffered"
911        {
912            "raw_event_stream"
913        } else {
914            consumption
915        };
916        ClientMethodPlan {
917            operation,
918            method_ident,
919            request_parameters,
920            generics,
921            response_type,
922            error_type: self.op_error_type_token(operation),
923            response: ClientResponsePlan {
924                statuses: success.statuses.iter().map(|s| (*s).to_string()).collect(),
925                excluded_statuses: success
926                    .excluded_statuses
927                    .iter()
928                    .map(|s| (*s).to_string())
929                    .collect(),
930                media_type: success.accept.map(str::to_owned),
931                kind: kind.to_string(),
932                consumption: consumption.to_string(),
933            },
934            builder_method_ident: None,
935            builder_type_ident: None,
936            success,
937            validate_content_type,
938        }
939    }
940
941    fn plan_operation_builders(
942        &self,
943        analysis: &SchemaAnalysis,
944        plans: &mut [ClientMethodPlan<'_>],
945    ) {
946        if !self.config().builders.enabled {
947            return;
948        }
949
950        let mut used_entry_methods: std::collections::HashSet<String> = plans
951            .iter()
952            .map(|plan| plan.method_ident.to_string())
953            .collect();
954        let mut used_type_names = std::collections::HashSet::new();
955        for schema_name in analysis.schemas.keys() {
956            let rust_name = self.to_rust_type_name(schema_name);
957            used_type_names.insert(rust_name.clone());
958            // Request-model builders live in `types.rs` and are imported by
959            // glob into the client module. Reserve their conventional names
960            // so operation builders cannot create ambiguous re-exports.
961            used_type_names.insert(format!("{rust_name}Builder"));
962        }
963        used_type_names.insert("HttpClient".to_string());
964        used_type_names.insert("ApiOpError".to_string());
965        used_type_names.extend(
966            [
967                "ClientBuilder",
968                "ClientWithMiddleware",
969                "RetryConfig",
970                "HttpError",
971                "BTreeMap",
972            ]
973            .into_iter()
974            .map(str::to_string),
975        );
976        for plan in plans.iter() {
977            let operation = plan.operation;
978            used_type_names.insert(self.op_error_enum_ident(operation).to_string());
979            used_type_names.extend(
980                operation
981                    .parameters
982                    .iter()
983                    .filter(|parameter| parameter.enum_values.is_some())
984                    .map(|parameter| parameter.rust_type.clone()),
985            );
986        }
987
988        for plan in plans.iter_mut() {
989            let operation = plan.operation;
990            let allocated_params = self.allocated_operation_params(operation);
991            let body_plan = self.body_model_plan(operation, analysis);
992            let optional_param_count = allocated_params
993                .iter()
994                .filter(|allocated| !Self::builder_param_is_required(allocated.param))
995                .count();
996            let optional_body_count =
997                usize::from(operation.request_body.is_some() && !operation.request_body_required);
998            let body_field_count = body_plan
999                .as_ref()
1000                .filter(|plan| {
1001                    operation.request_body_required
1002                        || matches!(
1003                            &plan.required_construction,
1004                            RequiredBodyConstruction::Default
1005                        )
1006                })
1007                .map_or(0, |plan| plan.optional_fields.len());
1008            let optional_count = optional_param_count + optional_body_count + body_field_count;
1009            if optional_count <= self.config().builders.threshold {
1010                continue;
1011            }
1012
1013            let flat_method = plan.method_ident.clone();
1014            let entry_base = format!("{flat_method}_builder");
1015            let entry_name = Self::allocate_name(&entry_base, &mut used_entry_methods);
1016            let entry_ident = Self::to_field_ident(&entry_name);
1017
1018            let builder_base = format!("{}Builder", flat_method.to_string().to_pascal_case());
1019            let builder_name = Self::allocate_type_name(&builder_base, &mut used_type_names);
1020            let builder_ident = format_ident!("{builder_name}");
1021
1022            plan.builder_method_ident = Some(entry_ident);
1023            plan.builder_type_ident = Some(builder_ident);
1024        }
1025    }
1026
1027    fn generate_operation_builders(
1028        &self,
1029        analysis: &SchemaAnalysis,
1030        plans: &[ClientMethodPlan<'_>],
1031    ) -> (Vec<TokenStream>, Vec<TokenStream>) {
1032        let mut definitions = Vec::new();
1033        let mut entries = Vec::new();
1034        for plan in plans {
1035            let (Some(entry_ident), Some(builder_ident)) =
1036                (&plan.builder_method_ident, &plan.builder_type_ident)
1037            else {
1038                continue;
1039            };
1040            let operation = plan.operation;
1041            let allocated_params = self.allocated_operation_params(operation);
1042            let (definition, entry) = self.generate_single_operation_builder(
1043                analysis,
1044                operation,
1045                &allocated_params,
1046                self.body_model_plan(operation, analysis),
1047                &plan.method_ident,
1048                entry_ident,
1049                builder_ident,
1050                plan,
1051            );
1052            definitions.push(definition);
1053            entries.push(entry);
1054        }
1055        (definitions, entries)
1056    }
1057
1058    #[allow(clippy::too_many_arguments)]
1059    fn generate_single_operation_builder(
1060        &self,
1061        _analysis: &SchemaAnalysis,
1062        operation: &OperationInfo,
1063        allocated_params: &[AllocatedOperationParam<'_>],
1064        body_plan: Option<BodyModelPlan>,
1065        flat_method: &syn::Ident,
1066        entry_ident: &syn::Ident,
1067        builder_ident: &syn::Ident,
1068        plan: &ClientMethodPlan<'_>,
1069    ) -> (TokenStream, TokenStream) {
1070        let mut fields = vec![quote! { client: &'a HttpClient }];
1071        let mut entry_parameters = Vec::new();
1072        let mut initializers = vec![quote! { client: self }];
1073        let mut call_arguments = Vec::new();
1074        let mut setters = Vec::new();
1075        let mut used_entry_params = std::collections::HashSet::new();
1076        let mut used_methods = std::collections::HashSet::from(["send".to_string()]);
1077
1078        for allocated in allocated_params {
1079            let field_ident = &allocated.ident;
1080            let storage_type = self.builder_param_storage_type(allocated.param);
1081            if Self::builder_param_is_required(allocated.param) {
1082                fields.push(quote! { #field_ident: #storage_type });
1083                let entry_name =
1084                    Self::allocate_name(&field_ident.to_string(), &mut used_entry_params);
1085                let entry_param = Self::to_field_ident(&entry_name);
1086                if Self::param_has_impl_as_ref_type(allocated.param) {
1087                    entry_parameters.push(quote! { #entry_param: impl Into<String> });
1088                    initializers.push(quote! { #field_ident: #entry_param.into() });
1089                } else {
1090                    entry_parameters.push(quote! { #entry_param: #storage_type });
1091                    initializers.push(quote! { #field_ident: #entry_param });
1092                }
1093            } else {
1094                fields.push(quote! { #field_ident: Option<#storage_type> });
1095                initializers.push(quote! { #field_ident: None });
1096                let setter_ident =
1097                    Self::allocate_builder_method(&field_ident.to_string(), &mut used_methods);
1098                let wire_name = &allocated.param.name;
1099                let assignment = if Self::param_has_impl_as_ref_type(allocated.param) {
1100                    quote! { self.#field_ident = Some(#field_ident.into()); }
1101                } else {
1102                    quote! { self.#field_ident = Some(#field_ident); }
1103                };
1104                let setter_type = if Self::param_has_impl_as_ref_type(allocated.param) {
1105                    quote! { impl Into<String> }
1106                } else {
1107                    storage_type.clone()
1108                };
1109                setters.push(quote! {
1110                    #[doc = concat!("Set the optional `", #wire_name, "` operation parameter.")]
1111                    #[must_use]
1112                    pub fn #setter_ident(mut self, #field_ident: #setter_type) -> Self {
1113                        #assignment
1114                        self
1115                    }
1116                });
1117            }
1118            call_arguments.push(quote! { self.#field_ident });
1119        }
1120
1121        if let Some(body_plan) = body_plan {
1122            let BodyModelPlan {
1123                body_ident,
1124                body_type,
1125                required_construction,
1126                optional_fields,
1127            } = body_plan;
1128            let can_initialize_optional_body =
1129                matches!(&required_construction, RequiredBodyConstruction::Default);
1130            if operation.request_body_required {
1131                fields.push(quote! { #body_ident: #body_type });
1132                match required_construction {
1133                    RequiredBodyConstruction::Default => {
1134                        initializers.push(quote! { #body_ident: Default::default() });
1135                    }
1136                    RequiredBodyConstruction::New(constructor_params) => {
1137                        let mut constructor_args = Vec::new();
1138                        for constructor in constructor_params {
1139                            let preferred = constructor.preferred_ident.to_string();
1140                            let entry_name =
1141                                Self::allocate_name(&preferred, &mut used_entry_params);
1142                            let entry_param = Self::to_field_ident(&entry_name);
1143                            let value_type = constructor.value_type;
1144                            entry_parameters.push(quote! { #entry_param: #value_type });
1145                            constructor_args.push(entry_param);
1146                        }
1147                        initializers.push(quote! {
1148                            #body_ident: #body_type::new(#(#constructor_args),*)
1149                        });
1150                    }
1151                    RequiredBodyConstruction::Whole => {
1152                        let entry_name =
1153                            Self::allocate_name(&body_ident.to_string(), &mut used_entry_params);
1154                        let entry_param = Self::to_field_ident(&entry_name);
1155                        entry_parameters.push(quote! { #entry_param: #body_type });
1156                        initializers.push(quote! { #body_ident: #entry_param });
1157                    }
1158                }
1159            } else {
1160                fields.push(quote! { #body_ident: Option<#body_type> });
1161                initializers.push(quote! { #body_ident: None });
1162            }
1163
1164            let body_setter =
1165                Self::allocate_builder_method(&body_ident.to_string(), &mut used_methods);
1166            let body_assignment = if operation.request_body_required {
1167                quote! { self.#body_ident = #body_ident; }
1168            } else {
1169                quote! { self.#body_ident = Some(#body_ident); }
1170            };
1171            setters.push(quote! {
1172                /// Replace the complete request body.
1173                #[must_use]
1174                pub fn #body_setter(mut self, #body_ident: #body_type) -> Self {
1175                    #body_assignment
1176                    self
1177                }
1178            });
1179
1180            if operation.request_body_required || can_initialize_optional_body {
1181                for field in optional_fields {
1182                    let setter_ident = Self::allocate_builder_method(
1183                        &field.preferred_method_name,
1184                        &mut used_methods,
1185                    );
1186                    let preferred_method_name = field.preferred_method_name.clone();
1187                    let value_ident = field.value_ident;
1188                    let value_type = field.value_type;
1189                    let wire_name = field.wire_name;
1190                    let access_path = field.access_path;
1191                    let assignment = if operation.request_body_required {
1192                        let mut target = quote! { self.#body_ident };
1193                        for access in &access_path {
1194                            target = quote! { #target.#access };
1195                        }
1196                        if field.tri_state {
1197                            quote! { #target = Some(Some(#value_ident)); }
1198                        } else {
1199                            quote! { #target = Some(#value_ident); }
1200                        }
1201                    } else {
1202                        let mut target = quote! { request };
1203                        for access in &access_path {
1204                            target = quote! { #target.#access };
1205                        }
1206                        if field.tri_state {
1207                            quote! {
1208                                let request = self.#body_ident.get_or_insert_with(Default::default);
1209                                #target = Some(Some(#value_ident));
1210                            }
1211                        } else {
1212                            quote! {
1213                                let request = self.#body_ident.get_or_insert_with(Default::default);
1214                                #target = Some(#value_ident);
1215                            }
1216                        }
1217                    };
1218                    setters.push(quote! {
1219                        #[doc = concat!("Set the optional request-body field `", #wire_name, "`.")]
1220                        #[must_use]
1221                        pub fn #setter_ident(mut self, #value_ident: #value_type) -> Self {
1222                            #assignment
1223                            self
1224                        }
1225                    });
1226                    if field.tri_state {
1227                        let null_ident = Self::allocate_builder_method(
1228                            &format!("{preferred_method_name}_null"),
1229                            &mut used_methods,
1230                        );
1231                        let absent_ident = Self::allocate_builder_method(
1232                            &format!("{preferred_method_name}_absent"),
1233                            &mut used_methods,
1234                        );
1235                        let null_assignment = if operation.request_body_required {
1236                            let mut target = quote! { self.#body_ident };
1237                            for access in &access_path {
1238                                target = quote! { #target.#access };
1239                            }
1240                            quote! { #target = Some(None); }
1241                        } else {
1242                            let mut target = quote! { request };
1243                            for access in &access_path {
1244                                target = quote! { #target.#access };
1245                            }
1246                            quote! {
1247                                let request = self.#body_ident.get_or_insert_with(Default::default);
1248                                #target = Some(None);
1249                            }
1250                        };
1251                        let absent_assignment = if operation.request_body_required {
1252                            let mut target = quote! { self.#body_ident };
1253                            for access in &access_path {
1254                                target = quote! { #target.#access };
1255                            }
1256                            quote! { #target = None; }
1257                        } else {
1258                            let mut target = quote! { request };
1259                            for access in &access_path {
1260                                target = quote! { #target.#access };
1261                            }
1262                            quote! {
1263                                if let Some(request) = self.#body_ident.as_mut() {
1264                                    #target = None;
1265                                }
1266                            }
1267                        };
1268                        setters.push(quote! {
1269                            #[doc = concat!("Set the optional nullable request-body field `", #wire_name, "` to JSON null.")]
1270                            #[must_use]
1271                            pub fn #null_ident(mut self) -> Self {
1272                                #null_assignment
1273                                self
1274                            }
1275
1276                            #[doc = concat!("Omit the optional nullable request-body field `", #wire_name, "`.")]
1277                            #[must_use]
1278                            pub fn #absent_ident(mut self) -> Self {
1279                                #absent_assignment
1280                                self
1281                            }
1282                        });
1283                    }
1284                }
1285            }
1286            call_arguments.push(quote! { self.#body_ident });
1287        }
1288
1289        let response_type = self.response_type_for_body(plan.success.body);
1290        let error_type = &plan.error_type;
1291        let operation_id = &operation.operation_id;
1292        let send = quote! { self.client.#flat_method(#(#call_arguments),*).await };
1293        // The entry point is deprecated with its operation. `send` calls the
1294        // deprecated method on the caller's behalf, who was warned at the entry.
1295        let deprecated = Self::deprecated_attribute(operation);
1296        let allow_deprecated = if operation.deprecated {
1297            quote! { #[allow(deprecated)] }
1298        } else {
1299            TokenStream::new()
1300        };
1301        let definition = quote! {
1302            #[doc = concat!("Additive request builder for `", #operation_id, "`.")]
1303            #[must_use]
1304            pub struct #builder_ident<'a> {
1305                #(#fields,)*
1306            }
1307
1308            impl<'a> #builder_ident<'a> {
1309                #(#setters)*
1310
1311                /// Send the request through the existing flat operation method.
1312                #allow_deprecated
1313                pub async fn send(self) -> Result<#response_type, ApiOpError<#error_type>> {
1314                    #send
1315                }
1316            }
1317        };
1318        let entry = quote! {
1319            #[doc = concat!("Start an additive builder for `", #operation_id, "`.")]
1320            #deprecated
1321            pub fn #entry_ident(
1322                &self,
1323                #(#entry_parameters),*
1324            ) -> #builder_ident<'_> {
1325                #builder_ident {
1326                    #(#initializers,)*
1327                }
1328            }
1329        };
1330        (definition, entry)
1331    }
1332
1333    fn allocate_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
1334        let raw = base.starts_with("r#");
1335        let base = base.strip_prefix("r#").unwrap_or(base);
1336        let mut candidate = base.to_string();
1337        let mut suffix = 2;
1338        while used.contains(&candidate) || used.contains(&format!("r#{candidate}")) {
1339            candidate = format!("{base}_{suffix}");
1340            suffix += 1;
1341        }
1342        used.insert(candidate.clone());
1343        if raw {
1344            format!("r#{candidate}")
1345        } else {
1346            candidate
1347        }
1348    }
1349
1350    fn allocate_type_name(base: &str, used: &mut std::collections::HashSet<String>) -> String {
1351        if used.insert(base.to_string()) {
1352            return base.to_string();
1353        }
1354
1355        let mut suffix = 2;
1356        loop {
1357            let candidate = format!("{base}{suffix}");
1358            if used.insert(candidate.clone()) {
1359                return candidate;
1360            }
1361            suffix += 1;
1362        }
1363    }
1364
1365    fn allocate_builder_method(
1366        preferred: &str,
1367        used: &mut std::collections::HashSet<String>,
1368    ) -> syn::Ident {
1369        let plain = preferred.strip_prefix("r#").unwrap_or(preferred);
1370        let base = if used.contains(preferred) {
1371            format!("with_{plain}")
1372        } else {
1373            preferred.to_string()
1374        };
1375        let allocated = Self::allocate_name(&base, used);
1376        Self::to_field_ident(&allocated)
1377    }
1378
1379    fn allocated_operation_params<'a>(
1380        &self,
1381        operation: &'a OperationInfo,
1382    ) -> Vec<AllocatedOperationParam<'a>> {
1383        // Analyzer resolves parameter collisions once. Every request path and
1384        // flat/builder signature uses those identifiers.
1385        let mut used: std::collections::HashSet<String> =
1386            ["request", "body", "req", "request_url", "client", "form"]
1387                .into_iter()
1388                .map(str::to_owned)
1389                .collect();
1390        let mut allocated = Vec::new();
1391        for location in ["path", "query", "header", "cookie"] {
1392            for parameter in &operation.parameters {
1393                if parameter.location != location {
1394                    continue;
1395                }
1396                let raw = self.param_ident_str(parameter);
1397                let chosen = Self::allocate_name(&raw, &mut used);
1398                allocated.push(AllocatedOperationParam {
1399                    param: parameter,
1400                    ident: Self::to_field_ident(&chosen),
1401                });
1402            }
1403        }
1404        allocated
1405    }
1406
1407    fn operation_param_ident(
1408        &self,
1409        operation: &OperationInfo,
1410        parameter: &ParameterInfo,
1411    ) -> String {
1412        self.allocated_operation_params(operation)
1413            .into_iter()
1414            .find(|allocated| std::ptr::eq(allocated.param, parameter))
1415            .map(|allocated| allocated.ident.to_string())
1416            .unwrap_or_else(|| self.param_ident_str(parameter))
1417    }
1418
1419    fn builder_param_is_required(parameter: &ParameterInfo) -> bool {
1420        // The existing flat signature always emits path parameters as bare
1421        // values. Invalid real-world specs sometimes omit `required: true`;
1422        // mirror the flat contract so builder delegation remains type-correct.
1423        parameter.location == "path" || parameter.required
1424    }
1425
1426    fn builder_param_storage_type(&self, parameter: &ParameterInfo) -> TokenStream {
1427        self.get_param_owned_rust_type(parameter)
1428    }
1429
1430    fn param_has_impl_as_ref_type(parameter: &ParameterInfo) -> bool {
1431        !matches!(
1432            &parameter.query_serialization,
1433            Some(
1434                crate::analysis::QuerySerialization::FormExplodedArray { .. }
1435                    | crate::analysis::QuerySerialization::FormArray { .. }
1436                    | crate::analysis::QuerySerialization::SimpleHeaderArray { .. },
1437            )
1438        ) && Self::param_uses_as_ref_str(parameter)
1439    }
1440
1441    fn body_model_plan(
1442        &self,
1443        operation: &OperationInfo,
1444        analysis: &SchemaAnalysis,
1445    ) -> Option<BodyModelPlan> {
1446        use crate::analysis::{RequestBodyContent, SchemaType};
1447
1448        let request_body = operation.request_body.as_ref()?;
1449        let (body_name, body_ident) = match request_body {
1450            RequestBodyContent::Json { schema_name, .. }
1451            | RequestBodyContent::FormUrlEncoded { schema_name, .. }
1452            | RequestBodyContent::Multipart { schema_name, .. } => {
1453                (schema_name.as_str(), format_ident!("request"))
1454            }
1455            RequestBodyContent::OctetStream { .. }
1456            | RequestBodyContent::Binary { .. }
1457            | RequestBodyContent::Unsupported { .. } => {
1458                return Some(BodyModelPlan {
1459                    body_ident: format_ident!("body"),
1460                    body_type: quote! { Vec<u8> },
1461                    required_construction: RequiredBodyConstruction::Whole,
1462                    optional_fields: Vec::new(),
1463                });
1464            }
1465            RequestBodyContent::TextPlain { .. } => {
1466                return Some(BodyModelPlan {
1467                    body_ident: format_ident!("body"),
1468                    body_type: quote! { String },
1469                    required_construction: RequiredBodyConstruction::Whole,
1470                    optional_fields: Vec::new(),
1471                });
1472            }
1473            RequestBodyContent::SchemaLess { .. } => return None,
1474        };
1475        let body_type_name = self.to_rust_type_name(body_name);
1476        let body_type = syn::Ident::new(&body_type_name, proc_macro2::Span::call_site());
1477        let Some((resolved_name, resolved_schema)) =
1478            self.resolve_reference_schema(body_name, analysis)
1479        else {
1480            return Some(BodyModelPlan {
1481                body_ident,
1482                body_type: quote! { #body_type },
1483                required_construction: RequiredBodyConstruction::Whole,
1484                optional_fields: Vec::new(),
1485            });
1486        };
1487
1488        let mut optional_fields = Vec::new();
1489        let mut stack = std::collections::HashSet::new();
1490        self.collect_optional_body_fields(
1491            resolved_name,
1492            Vec::new(),
1493            analysis,
1494            &mut stack,
1495            &mut optional_fields,
1496        );
1497
1498        let required_construction = match &resolved_schema.schema_type {
1499            // An object that declares variants beside its properties carries
1500            // the variant in a required field of its own, which neither
1501            // `Default` nor the required-properties constructor can supply.
1502            SchemaType::Object {
1503                variant: Some(_), ..
1504            } => RequiredBodyConstruction::Whole,
1505            SchemaType::Object {
1506                properties,
1507                required,
1508                additional_properties,
1509                ..
1510            } if !self.is_discriminated_variant(resolved_name, analysis) => {
1511                let emitted = self.emitted_object_properties(
1512                    resolved_name,
1513                    properties,
1514                    required,
1515                    additional_properties,
1516                    analysis,
1517                );
1518                let required_fields: Vec<_> = emitted
1519                    .iter()
1520                    .filter(|field| field.is_required)
1521                    .map(|field| BodyConstructorParam {
1522                        preferred_ident: field.ident.clone(),
1523                        value_type: field.field_type.clone(),
1524                    })
1525                    .collect();
1526                if required_fields.is_empty() {
1527                    RequiredBodyConstruction::Default
1528                } else if emitted.iter().any(|field| !field.is_required)
1529                    || additional_properties.is_open()
1530                {
1531                    RequiredBodyConstruction::New(required_fields)
1532                } else {
1533                    RequiredBodyConstruction::Whole
1534                }
1535            }
1536            _ => RequiredBodyConstruction::Whole,
1537        };
1538
1539        Some(BodyModelPlan {
1540            body_ident,
1541            body_type: quote! { #body_type },
1542            required_construction,
1543            optional_fields,
1544        })
1545    }
1546
1547    fn resolve_reference_schema<'a>(
1548        &self,
1549        schema_name: &'a str,
1550        analysis: &'a SchemaAnalysis,
1551    ) -> Option<(&'a str, &'a crate::analysis::AnalyzedSchema)> {
1552        let mut current = schema_name;
1553        let mut visited = std::collections::HashSet::new();
1554        loop {
1555            if !visited.insert(current) {
1556                return None;
1557            }
1558            let schema = analysis.schemas.get(current)?;
1559            if let crate::analysis::SchemaType::Reference { target } = &schema.schema_type {
1560                current = target;
1561            } else {
1562                return Some((current, schema));
1563            }
1564        }
1565    }
1566
1567    fn collect_optional_body_fields(
1568        &self,
1569        schema_name: &str,
1570        access_path: Vec<syn::Ident>,
1571        analysis: &SchemaAnalysis,
1572        stack: &mut std::collections::HashSet<String>,
1573        output: &mut Vec<BodyFieldPlan>,
1574    ) {
1575        use crate::analysis::SchemaType;
1576        if !stack.insert(schema_name.to_string()) {
1577            return;
1578        }
1579        let Some(schema) = analysis.schemas.get(schema_name) else {
1580            stack.remove(schema_name);
1581            return;
1582        };
1583        match &schema.schema_type {
1584            SchemaType::Reference { target } => {
1585                self.collect_optional_body_fields(target, access_path, analysis, stack, output);
1586            }
1587            SchemaType::Object {
1588                properties,
1589                required,
1590                additional_properties,
1591                ..
1592            } if !self.is_discriminated_variant(schema_name, analysis) => {
1593                for field in self.emitted_object_properties(
1594                    schema_name,
1595                    properties,
1596                    required,
1597                    additional_properties,
1598                    analysis,
1599                ) {
1600                    if field.is_required {
1601                        continue;
1602                    }
1603                    let mut field_path = access_path.clone();
1604                    field_path.push(field.ident.clone());
1605                    output.push(BodyFieldPlan {
1606                        wire_name: field.wire_name.to_string(),
1607                        preferred_method_name: field.ident.to_string(),
1608                        value_ident: field.ident.clone(),
1609                        value_type: self.generate_property_base_type(
1610                            schema_name,
1611                            field.wire_name,
1612                            field.property,
1613                            analysis,
1614                        ),
1615                        access_path: field_path,
1616                        tri_state: self.property_is_tri_state(
1617                            schema_name,
1618                            field.wire_name,
1619                            field.property,
1620                            field.is_required,
1621                        ),
1622                    });
1623                }
1624            }
1625            SchemaType::Composition { schemas } => {
1626                for (index, schema_ref) in schemas.iter().enumerate() {
1627                    let mut nested_path = access_path.clone();
1628                    nested_path.push(format_ident!("part_{index}"));
1629                    self.collect_optional_body_fields(
1630                        &schema_ref.target,
1631                        nested_path,
1632                        analysis,
1633                        stack,
1634                        output,
1635                    );
1636                }
1637            }
1638            _ => {}
1639        }
1640        stack.remove(schema_name);
1641    }
1642
1643    fn is_discriminated_variant(&self, schema_name: &str, analysis: &SchemaAnalysis) -> bool {
1644        analysis.schemas.values().any(|schema| {
1645            matches!(
1646                &schema.schema_type,
1647                crate::analysis::SchemaType::DiscriminatedUnion { variants, .. }
1648                    if variants.iter().any(|variant| variant.type_name == schema_name)
1649            )
1650        })
1651    }
1652
1653    /// Emit inline enum types for parameters whose schema is `type: string`
1654    /// with `enum` or `const`. The generated enum implements `Display` so it
1655    /// drops into the existing `format!`-based path/query templating without
1656    /// any special-casing at the call site. See issue #10 follow-up.
1657    fn generate_param_enum_types(&self, operations: &[&OperationInfo]) -> TokenStream {
1658        let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
1659        for op in operations {
1660            for param in &op.parameters {
1661                if param.enum_values.is_some() {
1662                    by_name.entry(param.rust_type.clone()).or_insert(param);
1663                }
1664            }
1665        }
1666
1667        if by_name.is_empty() {
1668            return quote! {};
1669        }
1670
1671        let defs: Vec<TokenStream> = by_name
1672            .values()
1673            .map(|param| self.generate_single_param_enum(param))
1674            .collect();
1675
1676        quote! { #(#defs)* }
1677    }
1678
1679    fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
1680        let Some(values) = param.enum_values.as_deref() else {
1681            return quote! {};
1682        };
1683
1684        let enum_ident = format_ident!("{}", param.rust_type);
1685
1686        // Dedupe variant names. Real-world specs use sort enums like
1687        // `["created_at", "-created_at"]` (descending prefix), and both
1688        // PascalCase to `CreatedAt`. Number collisions `CreatedAt2`, … while
1689        // keeping each `serde(rename)` pointing at the original wire string.
1690        let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1691        // `x-enum-varnames` wins over the naming heuristic when the spec
1692        // supplies it — the whole point of the extension is that the author
1693        // knows better than a transformation of the wire string. Schema-level
1694        // enums already honored it; parameter enums did not, so the same spec
1695        // produced different variant names depending on where its enum lived.
1696        // Suffix disambiguation still applies, since nothing stops a spec from
1697        // declaring two names that collide once converted to an identifier.
1698        let variant_names: Vec<String> = values
1699            .iter()
1700            .enumerate()
1701            .map(|(index, value)| {
1702                let base = param
1703                    .enum_varnames
1704                    .as_ref()
1705                    .and_then(|names| names.get(index))
1706                    .map(|name| self.to_rust_enum_variant(name))
1707                    .unwrap_or_else(|| self.to_rust_enum_variant(value));
1708                let mut chosen = base.clone();
1709                let mut suffix = 2;
1710                while !used.insert(chosen.clone()) {
1711                    chosen = crate::generator::numbered_variant_name(&base, suffix);
1712                    suffix += 1;
1713                }
1714                chosen
1715            })
1716            .collect();
1717
1718        let variants: Vec<TokenStream> = values
1719            .iter()
1720            .zip(&variant_names)
1721            .map(|(value, name)| {
1722                let variant_ident = format_ident!("{}", name);
1723                quote! {
1724                    #[serde(rename = #value)]
1725                    #variant_ident,
1726                }
1727            })
1728            .collect();
1729
1730        let display_arms: Vec<TokenStream> = values
1731            .iter()
1732            .zip(&variant_names)
1733            .map(|(value, name)| {
1734                let variant_ident = format_ident!("{}", name);
1735                quote! { Self::#variant_ident => #value, }
1736            })
1737            .collect();
1738
1739        let doc = format!(
1740            "Allowed values for the `{}` {} parameter.",
1741            param.name, param.location
1742        );
1743
1744        quote! {
1745            #[doc = #doc]
1746            #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1747            pub enum #enum_ident {
1748                #(#variants)*
1749            }
1750
1751            impl #enum_ident {
1752                pub fn as_str(&self) -> &'static str {
1753                    match self {
1754                        #(#display_arms)*
1755                    }
1756                }
1757            }
1758
1759            impl std::fmt::Display for #enum_ident {
1760                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1761                    f.write_str(self.as_str())
1762                }
1763            }
1764
1765            impl AsRef<str> for #enum_ident {
1766                fn as_ref(&self) -> &str {
1767                    self.as_str()
1768                }
1769            }
1770        }
1771    }
1772
1773    /// Generate the per-operation typed error enum, if the op has any non-2xx
1774    /// responses with a body schema. Returns None when the op has no declared
1775    /// error bodies — those operations use `ApiOpError<serde_json::Value>` so
1776    /// the raw response body is still inspectable.
1777    fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
1778        let variants: Vec<(String, String)> = op
1779            .response_schemas
1780            .iter()
1781            .filter(|(code, _)| !code.starts_with('2'))
1782            .map(|(code, schema)| (code.clone(), schema.clone()))
1783            .collect();
1784
1785        if variants.is_empty() {
1786            return None;
1787        }
1788
1789        let enum_ident = self.op_error_enum_ident(op);
1790        let variant_decls: Vec<TokenStream> = variants
1791            .iter()
1792            .map(|(code, schema)| {
1793                let variant_ident = Self::op_error_variant_ident(code);
1794                let payload_ty_name = self.to_rust_type_name(schema);
1795                let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
1796                quote! { #variant_ident(#payload_ty) }
1797            })
1798            .collect();
1799
1800        let doc = format!(
1801            "Typed error responses for `{}`. One variant per declared non-2xx response.",
1802            op.operation_id
1803        );
1804
1805        Some(quote! {
1806            #[doc = #doc]
1807            #[derive(Debug, Clone)]
1808            pub enum #enum_ident {
1809                #(#variant_decls,)*
1810            }
1811        })
1812    }
1813
1814    /// Type name (Ident) for the per-op error enum, e.g. `ListTodosApiError`.
1815    fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
1816        use heck::ToPascalCase;
1817        let name = format!(
1818            "{}ApiError",
1819            op.operation_id.replace('.', "_").to_pascal_case()
1820        );
1821        syn::Ident::new(&name, proc_macro2::Span::call_site())
1822    }
1823
1824    /// Variant name for a status code: "400" → Status400, "default" → Default,
1825    /// "4XX" → Status4xx.
1826    fn op_error_variant_ident(status_code: &str) -> syn::Ident {
1827        let raw = match status_code {
1828            "default" | "Default" => "Default".to_string(),
1829            other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
1830            other => format!("Status{}", other.to_ascii_lowercase()),
1831        };
1832        syn::Ident::new(&raw, proc_macro2::Span::call_site())
1833    }
1834
1835    /// Token stream for the type plugged into `ApiOpError<T>` for an op:
1836    /// either the per-op enum, or `serde_json::Value` for ops with no
1837    /// declared error body schemas.
1838    fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
1839        if op
1840            .response_schemas
1841            .iter()
1842            .any(|(code, _)| !code.starts_with('2'))
1843        {
1844            let ident = self.op_error_enum_ident(op);
1845            quote! { #ident }
1846        } else {
1847            quote! { serde_json::Value }
1848        }
1849    }
1850
1851    /// Generate a single operation method
1852    fn generate_planned_operation_method(
1853        &self,
1854        analysis: &SchemaAnalysis,
1855        plan: &ClientMethodPlan<'_>,
1856    ) -> TokenStream {
1857        let op = plan.operation;
1858        let method_name = &plan.method_ident;
1859        let http_method_call = self.http_method_call(op);
1860        let path = &op.path;
1861        let request_param = &plan.request_parameters;
1862        let generics = &plan.generics;
1863        let request_body = self.generate_request_body(op, analysis);
1864        let query_params = self.generate_query_params(op);
1865        let header_params = self.generate_header_params(op);
1866        let cookie_params = self.generate_cookie_params(op);
1867        let auth_application = self.generate_auth_application();
1868        let success = plan.success.clone();
1869        let response_type = &plan.response_type;
1870        let op_error_type = &plan.error_type;
1871        let accept = success.accept;
1872        let error_handling = self.generate_error_handling(op, success.clone());
1873        let content_validation = if plan.validate_content_type {
1874            self.generate_response_content_type_validation(op, &success)
1875        } else {
1876            TokenStream::new()
1877        };
1878        let (custom_headers, accept_header) = if let Some(media_type) = accept {
1879            (
1880                quote! {
1881                    for (name, value) in &self.custom_headers {
1882                        if !name.eq_ignore_ascii_case("accept") {
1883                            req = req.header(name, value);
1884                        }
1885                    }
1886                },
1887                quote! {
1888                    req = req.header(reqwest::header::ACCEPT, #media_type);
1889                },
1890            )
1891        } else {
1892            (
1893                quote! {
1894                    for (name, value) in &self.custom_headers {
1895                        req = req.header(name, value);
1896                    }
1897                },
1898                TokenStream::new(),
1899            )
1900        };
1901        let url_construction = self.generate_url_construction(path, op);
1902        let doc_comment = self.generate_operation_doc_comment(op);
1903        let variant_doc = if plan.validate_content_type {
1904            let description = format!(
1905                "Select `{}` as {}; accepts response statuses {}. Other successful statuses and mismatched Content-Type values are returned as inspectable API errors.",
1906                plan.response.media_type.as_deref().unwrap_or("no body"),
1907                plan.response.consumption,
1908                plan.response.statuses.join(", ")
1909            );
1910            quote! { #[doc = #description] }
1911        } else {
1912            TokenStream::new()
1913        };
1914        let stream_doc = if plan.response.consumption == "binary_stream" {
1915            quote! { #[doc = "Success chunks are returned live without a total body-size limit. Error responses use the configured buffered body-size limit."] }
1916        } else {
1917            TokenStream::new()
1918        };
1919
1920        let deprecated = Self::deprecated_attribute(op);
1921
1922        quote! {
1923            #doc_comment
1924            #variant_doc
1925            #stream_doc
1926            #deprecated
1927            pub async fn #method_name #generics(
1928                &self,
1929                #request_param
1930            ) -> Result<#response_type, ApiOpError<#op_error_type>> {
1931                #url_construction
1932
1933                let mut req = #http_method_call;
1934                #request_body
1935
1936                #query_params
1937                #header_params
1938                #cookie_params
1939
1940                // Apply configured authentication (T3). Was previously
1941                // hardcoded to bearer_auth regardless of GeneratorConfig.
1942                #auth_application
1943
1944                // Add custom headers
1945                #custom_headers
1946
1947                // Keep content negotiation aligned with the generated return type,
1948                // replacing any custom Accept value for this operation.
1949                #accept_header
1950
1951                let response = req.send().await?;
1952                #content_validation
1953                #error_handling
1954            }
1955        }
1956    }
1957
1958    /// T3: emit the auth-token application based on the configured AuthConfig.
1959    /// Default (no config) is Bearer on Authorization. ApiKey emits a custom
1960    /// header. Custom honors header_value_prefix.
1961    fn generate_auth_application(&self) -> TokenStream {
1962        use crate::http_config::AuthConfig;
1963        match &self.config().auth_config {
1964            Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
1965                if let Some(api_key) = &self.api_key {
1966                    req = req.bearer_auth(api_key);
1967                }
1968            },
1969            Some(AuthConfig::Bearer { header_name }) => {
1970                let h = header_name.clone();
1971                quote! {
1972                    if let Some(api_key) = &self.api_key {
1973                        req = req.header(#h, format!("Bearer {}", api_key));
1974                    }
1975                }
1976            }
1977            Some(AuthConfig::ApiKey { header_name }) => {
1978                let h = header_name.clone();
1979                quote! {
1980                    if let Some(api_key) = &self.api_key {
1981                        req = req.header(#h, api_key.as_str());
1982                    }
1983                }
1984            }
1985            Some(AuthConfig::Custom {
1986                header_name,
1987                header_value_prefix,
1988            }) => {
1989                let h = header_name.clone();
1990                let prefix = header_value_prefix.clone().unwrap_or_default();
1991                if prefix.is_empty() {
1992                    quote! {
1993                        if let Some(api_key) = &self.api_key {
1994                            req = req.header(#h, api_key.as_str());
1995                        }
1996                    }
1997                } else {
1998                    let format_str = format!("{}{{}}", prefix);
1999                    quote! {
2000                        if let Some(api_key) = &self.api_key {
2001                            req = req.header(#h, format!(#format_str, api_key));
2002                        }
2003                    }
2004                }
2005            }
2006            None => quote! {
2007                if let Some(api_key) = &self.api_key {
2008                    req = req.bearer_auth(api_key);
2009                }
2010            },
2011        }
2012    }
2013
2014    /// Generate header-parameter handling. Emits `req = req.header(name, ...)`
2015    /// for each `in: header` parameter — required headers unconditionally,
2016    /// optional ones gated on `Some(_)`.
2017    fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
2018        let header_params: Vec<_> = op
2019            .parameters
2020            .iter()
2021            .filter(|p| p.location == "header")
2022            .collect();
2023        if header_params.is_empty() {
2024            return quote! {};
2025        }
2026        let mut emit = Vec::new();
2027        for param in header_params {
2028            let param_name_snake = self.operation_param_ident(op, param);
2029            let param_ident = Self::to_field_ident(&param_name_snake);
2030            let header_name = &param.name;
2031            if matches!(
2032                param.query_serialization,
2033                Some(crate::analysis::QuerySerialization::SimpleHeaderArray { .. })
2034            ) {
2035                let encode = quote! {
2036                    v.iter().map(::std::string::ToString::to_string).collect::<Vec<_>>().join(",")
2037                };
2038                if param.required {
2039                    emit.push(quote! {
2040                        let v = #param_ident;
2041                        req = req.header(#header_name, #encode);
2042                    });
2043                } else {
2044                    emit.push(quote! {
2045                        if let Some(v) = #param_ident {
2046                            req = req.header(#header_name, #encode);
2047                        }
2048                    });
2049                }
2050                continue;
2051            }
2052            if param.required {
2053                if Self::param_uses_as_ref_str(param) {
2054                    emit.push(quote! {
2055                        req = req.header(#header_name, #param_ident.as_ref());
2056                    });
2057                } else {
2058                    emit.push(quote! {
2059                        req = req.header(#header_name, #param_ident.to_string());
2060                    });
2061                }
2062            } else if Self::param_uses_as_ref_str(param) {
2063                emit.push(quote! {
2064                    if let Some(v) = #param_ident {
2065                        req = req.header(#header_name, v.as_ref());
2066                    }
2067                });
2068            } else {
2069                emit.push(quote! {
2070                    if let Some(v) = #param_ident {
2071                        req = req.header(#header_name, v.to_string());
2072                    }
2073                });
2074            }
2075        }
2076        quote! {
2077            #(#emit)*
2078        }
2079    }
2080
2081    fn generate_cookie_params(&self, op: &OperationInfo) -> TokenStream {
2082        let cookie_params: Vec<_> = op
2083            .parameters
2084            .iter()
2085            .filter(|parameter| parameter.location == "cookie")
2086            .collect();
2087        if cookie_params.is_empty() {
2088            return quote! {};
2089        }
2090        let mut emit = Vec::new();
2091        for parameter in cookie_params {
2092            let ident = Self::to_field_ident(&self.operation_param_ident(op, parameter));
2093            let wire_name = parameter.name.as_str();
2094            if parameter.required {
2095                emit.push(quote! {
2096                    __cookie_fields.push(format!("{}={}", #wire_name, #ident));
2097                });
2098            } else {
2099                emit.push(quote! {
2100                    if let Some(value) = #ident {
2101                        __cookie_fields.push(format!("{}={}", #wire_name, value));
2102                    }
2103                });
2104            }
2105        }
2106        quote! {
2107            let mut __cookie_fields = Vec::new();
2108            #(#emit)*
2109            if !__cookie_fields.is_empty() {
2110                req = req.header(::reqwest::header::COOKIE, __cookie_fields.join("; "));
2111            }
2112        }
2113    }
2114
2115    /// Generate query parameter handling
2116    fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
2117        let query_params: Vec<_> = op
2118            .parameters
2119            .iter()
2120            .filter(|p| p.location == "query")
2121            .collect();
2122
2123        if query_params.is_empty() {
2124            return quote! {};
2125        }
2126
2127        let mut param_building = Vec::new();
2128        // Serialization applied on `req` directly, after the pair-vector
2129        // block: form-exploded objects and deepObject objects, whose keys
2130        // aren't the static parameter name.
2131        let mut req_appends = Vec::new();
2132
2133        for param in query_params {
2134            use crate::analysis::QuerySerialization;
2135
2136            // Use snake_case for Rust variable name with keyword escaping
2137            let param_name_snake = self.operation_param_ident(op, param);
2138            let param_name = Self::to_field_ident(&param_name_snake);
2139
2140            // Use the original parameter name from OpenAPI spec as the query string key
2141            let param_key = &param.name;
2142
2143            match &param.query_serialization {
2144                Some(QuerySerialization::FormExplodedNestedObject { properties }) => {
2145                    let emit_properties = properties.iter().map(|property| {
2146                        let wire_name = property.wire_name.as_str();
2147                        let field_ident = CodeGenerator::to_field_ident(
2148                            &self.to_rust_field_name(wire_name),
2149                        );
2150                        match &property.value_type {
2151                            crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
2152                                let value = serde_json::to_value(&v.#field_ident)
2153                                    .map_err(HttpError::serialization_error)?;
2154                                if !value.is_null() {
2155                                    let value = match value {
2156                                        serde_json::Value::String(value) => value,
2157                                        serde_json::Value::Bool(value) => value.to_string(),
2158                                        serde_json::Value::Number(value) => value.to_string(),
2159                                        _ => return Err(HttpError::serialization_error(
2160                                            format!("query field `{}` did not serialize as a scalar", #wire_name)
2161                                        ).into()),
2162                                    };
2163                                    nested_params.push((format!("{}.{}", #param_key, #wire_name), value));
2164                                }
2165                            },
2166                            crate::analysis::QueryStructPropertyType::Object { properties } => {
2167                                let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
2168                                quote! {
2169                                    let value = serde_json::to_value(&v.#field_ident)
2170                                        .map_err(HttpError::serialization_error)?;
2171                                    if !value.is_null() {
2172                                        let serde_json::Value::Object(object) = value else {
2173                                            return Err(HttpError::serialization_error(format!("query field `{}` did not serialize as an object", #wire_name)).into());
2174                                        };
2175                                        if object.is_empty() {
2176                                            nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
2177                                        } else {
2178                                            for leaf in [#(#leaves),*] {
2179                                                let Some(value) = object.get(leaf) else { continue };
2180                                                if value.is_null() { continue; }
2181                                                let value = match value {
2182                                                    serde_json::Value::String(value) => value.clone(),
2183                                                    serde_json::Value::Bool(value) => value.to_string(),
2184                                                    serde_json::Value::Number(value) => value.to_string(),
2185                                                    _ => return Err(HttpError::serialization_error(format!("query field `{}` contained a non-scalar leaf", #wire_name)).into()),
2186                                                };
2187                                                nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, leaf), value));
2188                                            }
2189                                        }
2190                                    }
2191                                }
2192                            }
2193                            crate::analysis::QueryStructPropertyType::Array { item_type } => {
2194                                let nested_properties = match item_type {
2195                                    crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => Some(
2196                                        properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>()
2197                                    ),
2198                                    _ => None,
2199                                };
2200                                if let Some(nested_properties) = nested_properties {
2201                                    quote! {
2202                                        let values = serde_json::to_value(&v.#field_ident)
2203                                            .map_err(HttpError::serialization_error)?;
2204                                        if !values.is_null() {
2205                                            let serde_json::Value::Array(values) = values else {
2206                                                return Err(HttpError::serialization_error(
2207                                                    format!("query field `{}` did not serialize as an array", #wire_name)
2208                                                ).into());
2209                                            };
2210                                            if values.is_empty() {
2211                                                nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
2212                                            }
2213                                            for (index, value) in values.into_iter().enumerate() {
2214                                                let serde_json::Value::Object(object) = value else {
2215                                                    return Err(HttpError::serialization_error(
2216                                                        format!("query field `{}` contained a non-object item", #wire_name)
2217                                                    ).into());
2218                                                };
2219                                                for leaf in [#(#nested_properties),*] {
2220                                                    let Some(value) = object.get(leaf) else { continue };
2221                                                    if value.is_null() { continue; }
2222                                                    let value = match value {
2223                                                        serde_json::Value::String(value) => value.clone(),
2224                                                        serde_json::Value::Bool(value) => value.to_string(),
2225                                                        serde_json::Value::Number(value) => value.to_string(),
2226                                                        _ => return Err(HttpError::serialization_error(
2227                                                            format!("query field `{}` contained a non-scalar leaf", #wire_name)
2228                                                        ).into()),
2229                                                    };
2230                                                    nested_params.push((format!("{}.{}.{}.{}", #param_key, #wire_name, index + 1, leaf), value));
2231                                                }
2232                                            }
2233                                        }
2234                                    }
2235                                } else {
2236                                    quote! {
2237                                        let values = serde_json::to_value(&v.#field_ident)
2238                                            .map_err(HttpError::serialization_error)?;
2239                                        if !values.is_null() {
2240                                            let serde_json::Value::Array(values) = values else {
2241                                                return Err(HttpError::serialization_error(
2242                                                    format!("query field `{}` did not serialize as an array", #wire_name)
2243                                                ).into());
2244                                            };
2245                                            if values.is_empty() {
2246                                                nested_params.push((format!("{}.{}[]", #param_key, #wire_name), String::new()));
2247                                            }
2248                                            for (index, value) in values.into_iter().enumerate() {
2249                                                let value = match value {
2250                                                    serde_json::Value::String(value) => value,
2251                                                    serde_json::Value::Bool(value) => value.to_string(),
2252                                                    serde_json::Value::Number(value) => value.to_string(),
2253                                                    _ => return Err(HttpError::serialization_error(
2254                                                        format!("query field `{}` contained a non-scalar item", #wire_name)
2255                                                    ).into()),
2256                                                };
2257                                                nested_params.push((format!("{}.{}.{}", #param_key, #wire_name, index + 1), value));
2258                                            }
2259                                        }
2260                                    }
2261                                }
2262                            }
2263                        }
2264                    }).collect::<Vec<_>>();
2265                    let apply = quote! {
2266                        let mut nested_params: Vec<(String, String)> = Vec::new();
2267                        #(#emit_properties)*
2268                        if nested_params.is_empty() {
2269                            nested_params.push((format!("{}[]", #param_key), String::new()));
2270                        }
2271                        req = req.query(&nested_params);
2272                    };
2273                    if param.required {
2274                        req_appends.push(quote! {{ let v = #param_name; #apply }});
2275                    } else {
2276                        req_appends.push(quote! { if let Some(v) = #param_name { #apply } });
2277                    }
2278                    continue;
2279                }
2280                Some(QuerySerialization::FormExplodedObject) => {
2281                    // Issue #27: reqwest serializes the struct through
2282                    // serde_urlencoded, so each property becomes its own
2283                    // `key=value` pair; the parameter's own name never
2284                    // appears in the query string (RFC 6570 form-explosion).
2285                    // `name[]=` is the shared zero-cardinality marker used to
2286                    // preserve Some(empty) and required-empty values.
2287                    let apply = quote! {
2288                        let __empty = match serde_json::to_value(&v)
2289                            .map_err(HttpError::serialization_error)?
2290                        {
2291                            serde_json::Value::Object(map) => map.is_empty(),
2292                            _ => false,
2293                        };
2294                        if __empty {
2295                            req = req.query(&[(format!("{}[]", #param_key), String::new())]);
2296                        } else {
2297                            req = req.query(&v);
2298                        }
2299                    };
2300                    if param.required {
2301                        req_appends.push(quote! {
2302                            {
2303                                let v = #param_name;
2304                                #apply
2305                            }
2306                        });
2307                    } else {
2308                        req_appends.push(quote! {
2309                            if let Some(v) = #param_name {
2310                                #apply
2311                            }
2312                        });
2313                    }
2314                    continue;
2315                }
2316                Some(QuerySerialization::DeepObject) => {
2317                    // `?filter[color]=red&filter[size]=5`. Property values
2318                    // stringify through their JSON form; Null (unset
2319                    // Option) properties are skipped.
2320                    let apply = quote! {
2321                        let map = match serde_json::to_value(&v)
2322                            .map_err(HttpError::serialization_error)?
2323                        {
2324                            serde_json::Value::Object(map) => map,
2325                            _ => return Err(HttpError::serialization_error(
2326                                format!("query parameter `{}` did not serialize as an object", #param_key)
2327                            ).into()),
2328                        };
2329                        let mut deep_params: Vec<(String, String)> = Vec::new();
2330                        for (k, val) in map {
2331                            let s = match val {
2332                                serde_json::Value::Null => continue,
2333                                serde_json::Value::String(s) => s,
2334                                other => other.to_string(),
2335                            };
2336                            deep_params.push((format!("{}[{}]", #param_key, k), s));
2337                        }
2338                        if deep_params.is_empty() {
2339                            deep_params.push((format!("{}[]", #param_key), String::new()));
2340                        }
2341                        req = req.query(&deep_params);
2342                    };
2343                    if param.required {
2344                        req_appends.push(quote! {
2345                            {
2346                                let v = #param_name;
2347                                #apply
2348                            }
2349                        });
2350                    } else {
2351                        req_appends.push(quote! {
2352                            if let Some(v) = #param_name {
2353                                #apply
2354                            }
2355                        });
2356                    }
2357                    continue;
2358                }
2359                Some(QuerySerialization::FormObject) => {
2360                    // `?filter=color,red,size,big` — one pair whose value is
2361                    // the comma-joined key,value list (RFC 6570 form,
2362                    // explode=false).
2363                    let apply = quote! {
2364                        let map = match serde_json::to_value(&v)
2365                            .map_err(HttpError::serialization_error)?
2366                        {
2367                            serde_json::Value::Object(map) => map,
2368                            _ => return Err(HttpError::serialization_error(
2369                                format!("query parameter `{}` did not serialize as an object", #param_key)
2370                            ).into()),
2371                        };
2372                        let mut parts: Vec<String> = Vec::new();
2373                        for (k, val) in map {
2374                            let s = match val {
2375                                serde_json::Value::Null => continue,
2376                                serde_json::Value::String(s) => s,
2377                                other => other.to_string(),
2378                            };
2379                            if k.contains(',') || s.contains(',') {
2380                                return Err(HttpError::serialization_error(
2381                                    format!(
2382                                        "query object `{}` contains a comma in key `{}`; use explode=true for lossless string values",
2383                                        #param_key,
2384                                        k,
2385                                    )
2386                                ).into());
2387                            }
2388                            parts.push(k);
2389                            parts.push(s);
2390                        }
2391                        if parts.is_empty() {
2392                            query_params.push((
2393                                format!("{}[]", #param_key),
2394                                String::new(),
2395                            ));
2396                        } else {
2397                            query_params.push((#param_key.to_string(), parts.join(",")));
2398                        }
2399                    };
2400                    if param.required {
2401                        param_building.push(quote! {
2402                            {
2403                                let v = #param_name;
2404                                #apply
2405                            }
2406                        });
2407                    } else {
2408                        param_building.push(quote! {
2409                            if let Some(v) = #param_name {
2410                                #apply
2411                            }
2412                        });
2413                    }
2414                    continue;
2415                }
2416                Some(QuerySerialization::FormExplodedArray { item_type }) => {
2417                    // `?tags=a&tags=b` — one pair per element; flat structures
2418                    // expand AWS query-protocol style as `?tags.1.Key=k&tags.1.Value=v`.
2419                    let struct_properties = match item_type {
2420                        crate::analysis::ArrayItemType::FlatStructRef { properties, .. }
2421                        | crate::analysis::ArrayItemType::NestedStructRef { properties, .. } => {
2422                            Some(properties.clone())
2423                        }
2424                        _ => None,
2425                    };
2426                    let emit_items = if let Some(properties) = struct_properties {
2427                        let pushes = properties
2428                            .iter()
2429                            .map(|property| {
2430                                let wire_name = &property.wire_name;
2431                                // Wire names such as `Type` land on struct
2432                                // fields via the same keyword-escaping the
2433                                // model generator uses (`r#type`).
2434                                let field_ident = CodeGenerator::to_field_ident(
2435                                    &self.to_rust_field_name(wire_name),
2436                                );
2437                                match &property.value_type {
2438                                    crate::analysis::QueryStructPropertyType::Scalar(_) => quote! {
2439                                        let value = serde_json::to_value(&item.#field_ident)
2440                                            .map_err(HttpError::serialization_error)?;
2441                                        if !value.is_null() {
2442                                            let value = match value {
2443                                                serde_json::Value::String(value) => value,
2444                                                serde_json::Value::Bool(value) => value.to_string(),
2445                                                serde_json::Value::Number(value) => value.to_string(),
2446                                                _ => return Err(HttpError::serialization_error(
2447                                                    format!("query field `{}.{}` did not serialize as a scalar", #param_key, #wire_name)
2448                                                ).into()),
2449                                            };
2450                                            query_params.push((
2451                                                format!("{}.{}.{}", #param_key, index, #wire_name),
2452                                                value,
2453                                            ));
2454                                        }
2455                                    },
2456                                    crate::analysis::QueryStructPropertyType::Object { properties } => {
2457                                        let leaves = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
2458                                        quote! {
2459                                            let value = serde_json::to_value(&item.#field_ident)
2460                                                .map_err(HttpError::serialization_error)?;
2461                                            if !value.is_null() {
2462                                                let serde_json::Value::Object(object) = value else {
2463                                                    return Err(HttpError::serialization_error(format!("query field `{}.{}` did not serialize as an object", #param_key, #wire_name)).into());
2464                                                };
2465                                                if object.is_empty() {
2466                                                    query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2467                                                }
2468                                                for leaf in [#(#leaves),*] {
2469                                                    let Some(value) = object.get(leaf) else { continue };
2470                                                    if value.is_null() { continue; }
2471                                                    let value = match value {
2472                                                        serde_json::Value::String(value) => value.clone(),
2473                                                        serde_json::Value::Bool(value) => value.to_string(),
2474                                                        serde_json::Value::Number(value) => value.to_string(),
2475                                                        _ => return Err(HttpError::serialization_error(format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)).into()),
2476                                                    };
2477                                                    query_params.push((format!("{}.{}.{}.{}", #param_key, index, #wire_name, leaf), value));
2478                                                }
2479                                            }
2480                                        }
2481                                    }
2482                                    crate::analysis::QueryStructPropertyType::Array { item_type } => {
2483                                        let nested_properties = match item_type {
2484                                            crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
2485                                                Some(properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>())
2486                                            }
2487                                            _ => None,
2488                                        };
2489                                        if let Some(nested_properties) = nested_properties {
2490                                            quote! {
2491                                                let values = serde_json::to_value(&item.#field_ident)
2492                                                    .map_err(HttpError::serialization_error)?;
2493                                                if !values.is_null() {
2494                                                    let serde_json::Value::Array(values) = values else {
2495                                                        return Err(HttpError::serialization_error(
2496                                                            format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
2497                                                        ).into());
2498                                                    };
2499                                                    if values.is_empty() {
2500                                                        query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2501                                                    }
2502                                                    for (nested_index, value) in values.into_iter().enumerate() {
2503                                                        let serde_json::Value::Object(object) = value else {
2504                                                            return Err(HttpError::serialization_error(
2505                                                                format!("query field `{}.{}` contained a non-object item", #param_key, #wire_name)
2506                                                            ).into());
2507                                                        };
2508                                                        if object.is_empty() {
2509                                                            query_params.push((format!("{}.{}.{}.{}[]", #param_key, index, #wire_name, nested_index + 1), String::new()));
2510                                                        }
2511                                                        for nested_wire_name in [#(#nested_properties),*] {
2512                                                            let Some(value) = object.get(nested_wire_name) else { continue };
2513                                                            if value.is_null() { continue; }
2514                                                            let value = match value {
2515                                                                serde_json::Value::String(value) => value.clone(),
2516                                                                serde_json::Value::Bool(value) => value.to_string(),
2517                                                                serde_json::Value::Number(value) => value.to_string(),
2518                                                                _ => return Err(HttpError::serialization_error(
2519                                                                    format!("query field `{}.{}` contained a non-scalar leaf", #param_key, #wire_name)
2520                                                                ).into()),
2521                                                            };
2522                                                            query_params.push((
2523                                                                format!("{}.{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1, nested_wire_name),
2524                                                                value,
2525                                                            ));
2526                                                        }
2527                                                    }
2528                                                }
2529                                            }
2530                                        } else {
2531                                            quote! {
2532                                                let values = serde_json::to_value(&item.#field_ident)
2533                                                    .map_err(HttpError::serialization_error)?;
2534                                                if !values.is_null() {
2535                                                    let serde_json::Value::Array(values) = values else {
2536                                                        return Err(HttpError::serialization_error(
2537                                                            format!("query field `{}.{}` did not serialize as an array", #param_key, #wire_name)
2538                                                        ).into());
2539                                                    };
2540                                                    if values.is_empty() {
2541                                                        query_params.push((format!("{}.{}.{}[]", #param_key, index, #wire_name), String::new()));
2542                                                    }
2543                                                    for (nested_index, value) in values.into_iter().enumerate() {
2544                                                        let value = match value {
2545                                                            serde_json::Value::String(value) => value,
2546                                                            serde_json::Value::Bool(value) => value.to_string(),
2547                                                            serde_json::Value::Number(value) => value.to_string(),
2548                                                            _ => return Err(HttpError::serialization_error(
2549                                                                format!("query field `{}.{}` contained a non-scalar item", #param_key, #wire_name)
2550                                                            ).into()),
2551                                                        };
2552                                                        query_params.push((
2553                                                            format!("{}.{}.{}.{}", #param_key, index, #wire_name, nested_index + 1),
2554                                                            value,
2555                                                        ));
2556                                                    }
2557                                                }
2558                                            }
2559                                        }
2560                                    }
2561                                }
2562                            })
2563                            .collect::<Vec<_>>();
2564                        quote! {
2565                            for (index, item) in v.iter().enumerate() {
2566                                let index = index + 1;
2567                                let item_start_len = query_params.len();
2568                                #(#pushes)*
2569                                if query_params.len() == item_start_len {
2570                                    query_params.push((format!("{}.{}[]", #param_key, index), String::new()));
2571                                }
2572                            }
2573                        }
2574                    } else {
2575                        quote! {
2576                            for item in v {
2577                                query_params.push((#param_key.to_string(), item.to_string()));
2578                            }
2579                        }
2580                    };
2581                    if param.required {
2582                        param_building.push(quote! {
2583                            let v = #param_name;
2584                            if v.is_empty() {
2585                                query_params.push((
2586                                    format!("{}[]", #param_key),
2587                                    String::new(),
2588                                ));
2589                            } else {
2590                                #emit_items
2591                            }
2592                        });
2593                    } else {
2594                        param_building.push(quote! {
2595                            if let Some(v) = #param_name {
2596                                if v.is_empty() {
2597                                    query_params.push((
2598                                        format!("{}[]", #param_key),
2599                                        String::new(),
2600                                    ));
2601                                } else {
2602                                    #emit_items
2603                                }
2604                            }
2605                        });
2606                    }
2607                    continue;
2608                }
2609                Some(QuerySerialization::FormArray { .. }) => {
2610                    // `?tags=a,b,c` — one comma-joined pair. Empty vectors
2611                    // use the shared `tags[]=` zero-cardinality marker.
2612                    let apply = quote! {
2613                        if v.is_empty() {
2614                            query_params.push((
2615                                format!("{}[]", #param_key),
2616                                String::new(),
2617                            ));
2618                        } else {
2619                            let mut parts = Vec::with_capacity(v.len());
2620                            for item in &v {
2621                                let item = item.to_string();
2622                                if item.contains(',') {
2623                                    return Err(HttpError::serialization_error(
2624                                        format!(
2625                                            "query array `{}` contains a comma; use explode=true for lossless string values",
2626                                            #param_key,
2627                                        )
2628                                    ).into());
2629                                }
2630                                parts.push(item);
2631                            }
2632                            query_params.push((
2633                                #param_key.to_string(),
2634                                parts.join(","),
2635                            ));
2636                        }
2637                    };
2638                    if param.required {
2639                        param_building.push(quote! {
2640                            {
2641                                let v = #param_name;
2642                                #apply
2643                            }
2644                        });
2645                    } else {
2646                        param_building.push(quote! {
2647                            if let Some(v) = #param_name {
2648                                #apply
2649                            }
2650                        });
2651                    }
2652                    continue;
2653                }
2654                Some(
2655                    QuerySerialization::Unsupported { .. }
2656                    | QuerySerialization::SimpleHeaderArray { .. },
2657                ) => {}
2658                None => {}
2659            }
2660
2661            if param.required {
2662                // Required parameters: always add
2663                if Self::param_uses_as_ref_str(param) {
2664                    param_building.push(quote! {
2665                        query_params.push((#param_key.to_string(), #param_name.as_ref().to_string()));
2666                    });
2667                } else {
2668                    param_building.push(quote! {
2669                        query_params.push((#param_key.to_string(), #param_name.to_string()));
2670                    });
2671                }
2672            } else {
2673                // Optional parameters: add only if Some
2674                if Self::param_uses_as_ref_str(param) {
2675                    param_building.push(quote! {
2676                        if let Some(v) = #param_name {
2677                            query_params.push((#param_key.to_string(), v.as_ref().to_string()));
2678                        }
2679                    });
2680                } else {
2681                    param_building.push(quote! {
2682                        if let Some(v) = #param_name {
2683                            query_params.push((#param_key.to_string(), v.to_string()));
2684                        }
2685                    });
2686                }
2687            }
2688        }
2689
2690        // Ops whose query params all serialize on `req` directly skip the
2691        // pair-vector block entirely.
2692        let pairs_block = if param_building.is_empty() {
2693            quote! {}
2694        } else {
2695            quote! {
2696                {
2697                    let mut query_params: Vec<(String, String)> = Vec::new();
2698                    #(#param_building)*
2699                    if !query_params.is_empty() {
2700                        req = req.query(&query_params);
2701                    }
2702                }
2703            }
2704        };
2705
2706        quote! {
2707            // Add query parameters
2708            #pairs_block
2709            #(#req_appends)*
2710        }
2711    }
2712
2713    /// `#[deprecated]` for an operation the document marks `deprecated: true`,
2714    /// so calling any of its client methods warns.
2715    fn deprecated_attribute(op: &OperationInfo) -> TokenStream {
2716        if op.deprecated {
2717            quote! { #[deprecated] }
2718        } else {
2719            TokenStream::new()
2720        }
2721    }
2722
2723    /// Generate the rustdoc block for an operation, surfacing summary,
2724    /// description, the HTTP method+path, and any tags from the OAS spec
2725    /// (T13). `deprecated_attribute` marks the method `#[deprecated]`.
2726    fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
2727        let method = op.method.to_uppercase();
2728        let path = &op.path;
2729        let mut docs: Vec<String> = Vec::new();
2730        if let Some(s) = &op.summary {
2731            if !s.is_empty() {
2732                docs.push(s.clone());
2733                docs.push(String::new());
2734            }
2735        }
2736        if let Some(d) = &op.description {
2737            if !d.is_empty() {
2738                for line in d.lines() {
2739                    docs.push(line.to_string());
2740                }
2741                docs.push(String::new());
2742            }
2743        }
2744        docs.push(format!("`{} {}`", method, path));
2745        let doc_attrs: Vec<TokenStream> = docs
2746            .iter()
2747            .map(|line| {
2748                let prefixed = if line.is_empty() {
2749                    String::new()
2750                } else {
2751                    format!(" {line}")
2752                };
2753                quote! { #[doc = #prefixed] }
2754            })
2755            .collect();
2756        quote! { #(#doc_attrs)* }
2757    }
2758
2759    /// Get the method name from the operation
2760    fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
2761        let name = if !op.operation_id.is_empty() {
2762            op.operation_id.to_snake_case()
2763        } else {
2764            // Fallback: generate from HTTP method and path
2765            format!(
2766                "{}_{}",
2767                op.method,
2768                op.path.replace('/', "_").replace(['{', '}'], "")
2769            )
2770            .to_snake_case()
2771        };
2772
2773        Self::to_field_ident(&self.escape_keyword_ident(&name))
2774    }
2775
2776    /// Build the request-builder expression for the operation's HTTP method.
2777    /// Named reqwest methods (`.get`/`.post`/…) are used where available;
2778    /// OPTIONS and TRACE go through `Client::request(Method::OPTIONS, _)` since
2779    /// reqwest doesn't expose those as named methods.
2780    fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
2781        match op.method.to_uppercase().as_str() {
2782            "GET" => quote! { self.http_client.get(request_url) },
2783            "POST" => quote! { self.http_client.post(request_url) },
2784            "PUT" => quote! { self.http_client.put(request_url) },
2785            "DELETE" => quote! { self.http_client.delete(request_url) },
2786            "PATCH" => quote! { self.http_client.patch(request_url) },
2787            "HEAD" => quote! { self.http_client.head(request_url) },
2788            "OPTIONS" => quote! {
2789                self.http_client.request(reqwest::Method::OPTIONS, request_url)
2790            },
2791            "TRACE" => quote! {
2792                self.http_client.request(reqwest::Method::TRACE, request_url)
2793            },
2794            // D1: 3.2 `QUERY` verb + any custom verb from
2795            // PathItem.additionalOperations. reqwest's Method::from_bytes
2796            // accepts arbitrary uppercase tokens that match the RFC7230
2797            // method grammar.
2798            other => {
2799                let upper = other.to_string();
2800                quote! {
2801                    self.http_client.request(
2802                        reqwest::Method::from_bytes(#upper.as_bytes())
2803                            .expect("invalid HTTP method"),
2804                        request_url,
2805                    )
2806                }
2807            }
2808        }
2809    }
2810
2811    /// Generate request parameters including path, query, header, and request body.
2812    fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
2813        self.generate_request_param_with_capture(op, None)
2814    }
2815
2816    fn generate_request_param_with_capture(
2817        &self,
2818        op: &OperationInfo,
2819        mut captures: Option<&mut Vec<syn::Ident>>,
2820    ) -> TokenStream {
2821        let mut params = Vec::new();
2822        for allocated in self.allocated_operation_params(op) {
2823            let param_name = allocated.ident;
2824            let param_type = if Self::param_has_impl_as_ref_type(allocated.param) {
2825                if let Some(captures) = captures.as_deref_mut() {
2826                    let ident = format_ident!("__Param{}", captures.len());
2827                    captures.push(ident.clone());
2828                    quote! { #ident }
2829                } else {
2830                    self.get_param_rust_type(allocated.param)
2831                }
2832            } else {
2833                self.get_param_rust_type(allocated.param)
2834            };
2835            if Self::builder_param_is_required(allocated.param) {
2836                params.push(quote! { #param_name: #param_type });
2837            } else {
2838                params.push(quote! { #param_name: Option<#param_type> });
2839            }
2840        }
2841
2842        // Add request body parameter based on content type. Optional bodies
2843        // (`requestBody.required` is false or absent) become `Option<T>` per T11.
2844        if let Some(ref rb) = op.request_body {
2845            use crate::analysis::RequestBodyContent;
2846            if matches!(rb, RequestBodyContent::SchemaLess { .. }) {
2847                return if params.is_empty() {
2848                    quote! {}
2849                } else {
2850                    quote! { #(#params),* }
2851                };
2852            }
2853            let required = op.request_body_required;
2854            let body_type = match rb {
2855                RequestBodyContent::Json { schema_name, .. }
2856                | RequestBodyContent::FormUrlEncoded { schema_name, .. }
2857                | RequestBodyContent::Multipart { schema_name, .. } => {
2858                    let rust_type_name = self.to_rust_type_name(schema_name);
2859                    let request_ident =
2860                        syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
2861                    quote! { #request_ident }
2862                }
2863
2864                RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. } => {
2865                    quote! { Vec<u8> }
2866                }
2867                RequestBodyContent::TextPlain { .. } => quote! { String },
2868                RequestBodyContent::Unsupported { .. } => quote! { Vec<u8> },
2869                RequestBodyContent::SchemaLess { .. } => unreachable!(
2870                    "schema-less request bodies preserve the historical client signature"
2871                ),
2872            };
2873            let body_ident = match rb {
2874                RequestBodyContent::OctetStream { .. }
2875                | RequestBodyContent::Binary { .. }
2876                | RequestBodyContent::TextPlain { .. }
2877                | RequestBodyContent::Unsupported { .. } => quote! { body },
2878                RequestBodyContent::SchemaLess { .. } => unreachable!(
2879                    "schema-less request bodies preserve the historical client signature"
2880                ),
2881                _ => quote! { request },
2882            };
2883            if required {
2884                params.push(quote! { #body_ident: #body_type });
2885            } else {
2886                params.push(quote! { #body_ident: Option<#body_type> });
2887            }
2888        }
2889
2890        if params.is_empty() {
2891            quote! {}
2892        } else {
2893            quote! { #(#params),* }
2894        }
2895    }
2896
2897    /// Get the Rust type for a parameter
2898    fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
2899        if Self::param_has_impl_as_ref_type(param) {
2900            quote! { impl AsRef<str> }
2901        } else {
2902            self.get_param_owned_rust_type(param)
2903        }
2904    }
2905
2906    /// Owned parameter type shared by client-builder storage and generated
2907    /// server extraction. [`ParameterInfo::query_serialization`] is the
2908    /// authoritative projection for typed query objects and arrays.
2909    pub(crate) fn get_param_owned_rust_type(
2910        &self,
2911        param: &crate::analysis::ParameterInfo,
2912    ) -> TokenStream {
2913        use crate::analysis::QuerySerialization;
2914        // Typed form-style arrays take Vec<item> (openapi-generator-anu).
2915        // Scalars parse as-is (they may be type paths from [type_mappings]);
2916        // schema refs are raw schema names and go through the same
2917        // to_rust_type_name sanitization as every other schema reference
2918        // (cloudflare has enum schemas like `resource-sharing_resource_type`).
2919        if let Some(
2920            QuerySerialization::FormExplodedArray { item_type }
2921            | QuerySerialization::FormArray { item_type }
2922            | QuerySerialization::SimpleHeaderArray { item_type },
2923        ) = &param.query_serialization
2924        {
2925            use crate::analysis::ArrayItemType;
2926            let item_ty: syn::Type = match item_type {
2927                ArrayItemType::Scalar(rust_type) => syn::parse_str(rust_type)
2928                    .unwrap_or_else(|_| panic!("invalid scalar item type `{rust_type}`")),
2929                ArrayItemType::SchemaRef(schema_name) => {
2930                    let rust_name = self.to_rust_type_name(schema_name);
2931                    syn::parse_str(&rust_name)
2932                        .unwrap_or_else(|_| panic!("invalid schema item type `{rust_name}`"))
2933                }
2934                ArrayItemType::FlatStructRef { schema_name, .. } => {
2935                    let rust_name = self.to_rust_type_name(schema_name);
2936                    syn::parse_str(&rust_name)
2937                        .unwrap_or_else(|_| panic!("invalid struct item type `{rust_name}`"))
2938                }
2939                ArrayItemType::NestedStructRef { schema_name, .. } => {
2940                    let rust_name = self.to_rust_type_name(schema_name);
2941                    syn::parse_str(&rust_name)
2942                        .unwrap_or_else(|_| panic!("invalid nested struct item type `{rust_name}`"))
2943                }
2944            };
2945            return quote! { Vec<#item_ty> };
2946        }
2947        // T10: $ref-typed parameters used to lose their type because we only
2948        // consulted `rust_type` (which stays "String"). Now: prefer the
2949        // resolved schema reference if present.
2950        if let Some(ref schema_name) = param.schema_ref {
2951            let rust_name = self.to_rust_type_name(schema_name);
2952            let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
2953            return quote! { #ident };
2954        }
2955        syn::parse_str::<syn::Type>(&param.rust_type)
2956            .map(|ty| quote! { #ty })
2957            .unwrap_or_else(|_| {
2958                let type_ident = syn::Ident::new(&param.rust_type, proc_macro2::Span::call_site());
2959                quote! { #type_ident }
2960            })
2961    }
2962
2963    /// True when the parameter's compile-time type is `impl AsRef<str>` and
2964    /// we should call `.as_ref()` on it before stringifying. False for any
2965    /// $ref-resolved type (T10) or non-String primitive — those just call
2966    /// `.to_string()`.
2967    fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
2968        param.schema_ref.is_none() && param.rust_type == "String"
2969    }
2970
2971    fn multipart_client_field_kind(
2972        schema_type: &crate::analysis::SchemaType,
2973        analysis: &SchemaAnalysis,
2974        visited: &mut std::collections::HashSet<String>,
2975    ) -> Option<MultipartClientFieldKind> {
2976        match schema_type {
2977            crate::analysis::SchemaType::Primitive {
2978                rust_type,
2979                serde_with,
2980            } => {
2981                let rust_type = rust_type.replace(' ', "");
2982                if rust_type == "bytes::Bytes" {
2983                    Some(MultipartClientFieldKind::RawBytes)
2984                } else if serde_with
2985                    .as_deref()
2986                    .is_some_and(|codec| codec.contains("base64_url"))
2987                {
2988                    Some(MultipartClientFieldKind::Base64UrlUnpadded)
2989                } else if serde_with
2990                    .as_deref()
2991                    .is_some_and(|codec| codec.contains("base64"))
2992                    || rust_type == "Vec<u8>"
2993                {
2994                    Some(MultipartClientFieldKind::Base64)
2995                } else {
2996                    Some(MultipartClientFieldKind::Text)
2997                }
2998            }
2999            crate::analysis::SchemaType::StringEnum { .. }
3000            | crate::analysis::SchemaType::ExtensibleEnum { .. } => {
3001                Some(MultipartClientFieldKind::Text)
3002            }
3003            // OpenAPI's default `contentType` for an object is
3004            // `application/json`. Arrays are left out: their default depends
3005            // on the items, and each item is a part of its own.
3006            crate::analysis::SchemaType::Object { .. }
3007            | crate::analysis::SchemaType::DiscriminatedUnion { .. }
3008            | crate::analysis::SchemaType::Union { .. }
3009            | crate::analysis::SchemaType::Composition { .. }
3010            | crate::analysis::SchemaType::Untyped {
3011                shape:
3012                    crate::analysis::UntypedShape::Value | crate::analysis::UntypedShape::ValueMap,
3013                ..
3014            } => Some(MultipartClientFieldKind::Json),
3015            crate::analysis::SchemaType::Reference { target } => {
3016                if !visited.insert(target.clone()) {
3017                    return None;
3018                }
3019                analysis.schemas.get(target).and_then(|schema| {
3020                    Self::multipart_client_field_kind(&schema.schema_type, analysis, visited)
3021                })
3022            }
3023            _ => None,
3024        }
3025    }
3026
3027    fn generate_response_content_type_validation(
3028        &self,
3029        operation: &OperationInfo,
3030        success: &ClientSuccessSelection<'_>,
3031    ) -> TokenStream {
3032        let Some(expected) = success.accept else {
3033            return TokenStream::new();
3034        };
3035        let guard = self.success_selection_guard(operation, success);
3036        let errors = self.generate_error_match_arms(operation);
3037        let error_type = self.op_error_type_token(operation);
3038        let raw_body = if matches!(
3039            success.body,
3040            ClientSuccessBody::EventStream | ClientSuccessBody::ParsedSse
3041        ) {
3042            quote! { Vec::new() }
3043        } else {
3044            quote! { __read_bounded_response_body(response, self.max_response_body_bytes).await? }
3045        };
3046        quote! {
3047            let status = response.status();
3048            let status_code = status.as_u16();
3049            if #guard {
3050                let actual = response.headers().get(reqwest::header::CONTENT_TYPE).and_then(|value| value.to_str().ok()).unwrap_or("").to_string();
3051                let expected = #expected;
3052                let parse_media = |value: &str| {
3053                    let mut parts = Vec::new();
3054                    let mut start = 0;
3055                    let mut quoted = false;
3056                    let mut escaped = false;
3057                    for (index, character) in value.char_indices() {
3058                        if escaped { escaped = false; continue }
3059                        if character == '\\' && quoted { escaped = true; continue }
3060                        if character == '"' { quoted = !quoted; }
3061                        if character == ';' && !quoted { parts.push(&value[start..index]); start = index + 1; }
3062                    }
3063                    if quoted || escaped { return None }
3064                    parts.push(&value[start..]);
3065                    let essence = parts.first().copied().unwrap_or("").trim().to_ascii_lowercase();
3066                    let mut parameters = std::collections::BTreeMap::new();
3067                    for part in parts.into_iter().skip(1) {
3068                        let (name, value) = part.split_once('=')?;
3069                        let name = name.trim().to_ascii_lowercase();
3070                        if name.is_empty() { return None }
3071                        let value = value.trim();
3072                        let value = if let Some(quoted) = value.strip_prefix('"') {
3073                            let inner = quoted.strip_suffix('"')?;
3074                            let mut decoded = String::new();
3075                            let mut escaped = false;
3076                            for character in inner.chars() {
3077                                if escaped { decoded.push(character); escaped = false; }
3078                                else if character == '\\' { escaped = true; }
3079                                else if character == '"' { return None }
3080                                else { decoded.push(character); }
3081                            }
3082                            if escaped { return None }
3083                            decoded
3084                        } else {
3085                            if value.contains('"') { return None }
3086                            value.to_string()
3087                        };
3088                        if parameters.insert(name, value).is_some() { return None }
3089                    }
3090                    Some((essence, parameters))
3091                };
3092                let matches = match (parse_media(&actual), parse_media(expected)) {
3093                    (Some((actual_essence, actual_parameters)), Some((expected_essence, expected_parameters))) => {
3094                        let essence_matches = if expected_essence == "*/*" { actual_essence.contains('/') }
3095                            else if let Some(prefix) = expected_essence.strip_suffix("/*") { actual_essence.split('/').next() == Some(prefix) }
3096                            else { actual_essence == expected_essence };
3097                        essence_matches && expected_parameters.iter().all(|(name, value)| actual_parameters.get(name).is_some_and(|actual|
3098                            if name == "charset" { actual.eq_ignore_ascii_case(value) } else { actual == value }))
3099                    }
3100                    _ => false,
3101                };
3102                if !matches {
3103                    let headers = response.headers().clone();
3104                    let raw_body = #raw_body;
3105                    let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3106                    let typed: Option<#error_type>;
3107                    let parse_error: Option<String>;
3108                    #errors
3109                    let _ = parse_error;
3110                    return Err(ApiOpError::Api(ApiError {
3111                        status: status_code, headers, body: body_text, raw_body, typed,
3112                        parse_error: Some(format!("unexpected response Content-Type `{}`; expected `{}`", actual, expected)),
3113                    }));
3114                }
3115            }
3116        }
3117    }
3118
3119    /// The item type of a multipart field that is a list, directly or through
3120    /// named types such as `type Extensions = Vec<Extension>`.
3121    fn resolve_multipart_list<'a>(
3122        schema_type: &'a crate::analysis::SchemaType,
3123        analysis: &'a SchemaAnalysis,
3124    ) -> Option<&'a crate::analysis::SchemaType> {
3125        let mut current = schema_type;
3126        let mut visited = std::collections::HashSet::new();
3127        loop {
3128            match current {
3129                crate::analysis::SchemaType::Array { item_type } => return Some(item_type),
3130                crate::analysis::SchemaType::Reference { target } if visited.insert(target) => {
3131                    current = &analysis.schemas.get(target)?.schema_type;
3132                }
3133                _ => return None,
3134            }
3135        }
3136    }
3137
3138    /// Whether a multipart field holds a `MultipartFile`, the type the
3139    /// analysis gives file parts, under whatever name it gave it.
3140    fn is_multipart_file_type(
3141        schema_type: &crate::analysis::SchemaType,
3142        analysis: &SchemaAnalysis,
3143    ) -> bool {
3144        match schema_type {
3145            crate::analysis::SchemaType::Primitive { rust_type, .. } => {
3146                analysis.multipart_file_type.as_deref() == Some(rust_type.as_str())
3147            }
3148            crate::analysis::SchemaType::Nullable { inner_type } => {
3149                Self::is_multipart_file_type(inner_type, analysis)
3150            }
3151            _ => false,
3152        }
3153    }
3154
3155    /// The media type a multipart `encoding.<field>.contentType` gives its
3156    /// part, when it names exactly one. A comma-separated list or a range
3157    /// such as `image/*` only says what the server accepts.
3158    fn single_media_type(content_type: &str) -> Option<&str> {
3159        let content_type = content_type.trim();
3160        let essence = content_type.split(';').next().unwrap_or_default();
3161        (!content_type.contains(',') && !essence.contains('*') && essence.contains('/'))
3162            .then_some(content_type)
3163    }
3164
3165    fn generate_typed_multipart_form(
3166        &self,
3167        schema_name: &str,
3168        encoding: &BTreeMap<String, String>,
3169        analysis: &SchemaAnalysis,
3170    ) -> TokenStream {
3171        use crate::analysis::{ObjectAdditionalProperties, SchemaType};
3172
3173        let Some((resolved_name, resolved_schema)) =
3174            self.resolve_reference_schema(schema_name, analysis)
3175        else {
3176            let message = format!(
3177                "multipart request schema `{schema_name}` could not be resolved during generation"
3178            );
3179            return quote! {
3180                return Err(HttpError::Config(#message.to_string()).into());
3181            };
3182        };
3183        let SchemaType::Object {
3184            properties,
3185            required,
3186            additional_properties,
3187            ..
3188        } = &resolved_schema.schema_type
3189        else {
3190            let message =
3191                format!("multipart request schema `{schema_name}` must resolve to an object");
3192            return quote! {
3193                return Err(HttpError::Config(#message.to_string()).into());
3194            };
3195        };
3196        let fields = self.emitted_object_properties(
3197            resolved_name,
3198            properties,
3199            required,
3200            additional_properties,
3201            analysis,
3202        );
3203        if matches!(
3204            additional_properties,
3205            ObjectAdditionalProperties::Typed { .. }
3206        ) {
3207            let message = format!(
3208                "multipart request schema `{schema_name}` cannot contain typed additional properties"
3209            );
3210            return quote! {
3211                return Err(HttpError::Config(#message.to_string()).into());
3212            };
3213        }
3214
3215        let mut parts = Vec::new();
3216        for field in fields {
3217            let wire_name = field.wire_name;
3218            let is_nullable = self.property_is_nullable(resolved_name, wire_name, field.property);
3219            let is_tri_state = self.property_is_tri_state(
3220                resolved_name,
3221                wire_name,
3222                field.property,
3223                field.is_required,
3224            );
3225            let ident = field.ident;
3226            // A list is one part per item, under the field's name.
3227            let field_type = match &field.property.schema_type {
3228                crate::analysis::SchemaType::Nullable { inner_type } => &**inner_type,
3229                other => other,
3230            };
3231            let (is_array, item_type) = match Self::resolve_multipart_list(field_type, analysis) {
3232                Some(item_type) => (true, item_type),
3233                None => (false, field_type),
3234            };
3235            // A null item is no part, as a null field is none.
3236            let (nullable_items, item_type) = match item_type {
3237                crate::analysis::SchemaType::Nullable { inner_type } if is_array => {
3238                    (true, &**inner_type)
3239                }
3240                other => (false, other),
3241            };
3242            // A single media type in `encoding.<field>.contentType` is the
3243            // part's type, whatever the field holds. Several of them are the
3244            // set the server accepts, so none is picked.
3245            let declared_content_type = encoding
3246                .get(wire_name)
3247                .and_then(|content_type| Self::single_media_type(content_type));
3248            let add_item = if Self::is_multipart_file_type(item_type, analysis) {
3249                // A file's own content type wins over the declared one.
3250                let part = match self.config().types.binary {
3251                    crate::type_mapping::BinaryStrategy::Bytes => {
3252                        quote! { reqwest::multipart::Part::bytes(value.content.to_vec()) }
3253                    }
3254                    crate::type_mapping::BinaryStrategy::VecU8 => {
3255                        quote! { reqwest::multipart::Part::bytes(value.content.clone()) }
3256                    }
3257                    crate::type_mapping::BinaryStrategy::String => {
3258                        quote! { reqwest::multipart::Part::text(value.content.clone()) }
3259                    }
3260                };
3261                let invalid = format!("multipart field `{wire_name}` has an invalid content type");
3262                let set_content_type = quote! {
3263                    part = part.mime_str(content_type).map_err(|error| {
3264                        HttpError::Config(format!("{} `{}`: {}", #invalid, content_type, error))
3265                    })?;
3266                };
3267                let content_type = match declared_content_type {
3268                    Some(declared) => quote! {
3269                        let content_type = value.content_type.as_deref().unwrap_or(#declared);
3270                        #set_content_type
3271                    },
3272                    None => quote! {
3273                        if let Some(content_type) = value.content_type.as_deref() {
3274                            #set_content_type
3275                        }
3276                    },
3277                };
3278                quote! {
3279                    let mut part = #part;
3280                    if let Some(file_name) = &value.file_name {
3281                        part = part.file_name(file_name.clone());
3282                    }
3283                    #content_type
3284                    form = form.part(#wire_name, part);
3285                }
3286            } else {
3287                let Some(kind) = Self::multipart_client_field_kind(
3288                    item_type,
3289                    analysis,
3290                    &mut std::collections::HashSet::new(),
3291                ) else {
3292                    let message = format!(
3293                        "multipart field `{wire_name}` must be a file, an object, a scalar, or a list of them"
3294                    );
3295                    return quote! {
3296                        return Err(HttpError::Config(#message.to_string()).into());
3297                    };
3298                };
3299                let content_type =
3300                    declared_content_type.or(matches!(kind, MultipartClientFieldKind::Json)
3301                        .then_some("application/json"));
3302                // The shorthand for a part that needs no content type. An
3303                // object always has one.
3304                let shorthand = match kind {
3305                    MultipartClientFieldKind::RawBytes => Some(quote! {
3306                        form = form.part(#wire_name, reqwest::multipart::Part::bytes(value.to_vec()));
3307                    }),
3308                    MultipartClientFieldKind::Base64 => Some(quote! {
3309                        use base64::Engine as _;
3310                        form = form.text(#wire_name, base64::engine::general_purpose::STANDARD.encode(value));
3311                    }),
3312                    MultipartClientFieldKind::Base64UrlUnpadded => Some(quote! {
3313                        use base64::Engine as _;
3314                        form = form.text(#wire_name, base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value));
3315                    }),
3316                    MultipartClientFieldKind::Text => {
3317                        Some(quote! { form = form.text(#wire_name, value.to_string()); })
3318                    }
3319                    MultipartClientFieldKind::Json => None,
3320                }
3321                .filter(|_| content_type.is_none());
3322                if let Some(shorthand) = shorthand {
3323                    shorthand
3324                } else {
3325                    let part = match kind {
3326                        MultipartClientFieldKind::RawBytes => {
3327                            quote! { reqwest::multipart::Part::bytes(value.to_vec()) }
3328                        }
3329                        MultipartClientFieldKind::Base64 => quote! { {
3330                            use base64::Engine as _;
3331                            reqwest::multipart::Part::text(base64::engine::general_purpose::STANDARD.encode(value))
3332                        } },
3333                        MultipartClientFieldKind::Base64UrlUnpadded => quote! { {
3334                            use base64::Engine as _;
3335                            reqwest::multipart::Part::text(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value))
3336                        } },
3337                        MultipartClientFieldKind::Text => {
3338                            quote! { reqwest::multipart::Part::text(value.to_string()) }
3339                        }
3340                        MultipartClientFieldKind::Json => quote! {
3341                            reqwest::multipart::Part::bytes(serde_json::to_vec(value).map_err(HttpError::serialization_error)?)
3342                        },
3343                    };
3344                    let content_type = content_type.map(|content_type| {
3345                        let message = format!(
3346                            "multipart field `{wire_name}` has an invalid content type `{content_type}`"
3347                        );
3348                        quote! {
3349                            let part = part
3350                                .mime_str(#content_type)
3351                                .map_err(|error| HttpError::Config(format!("{}: {}", #message, error)))?;
3352                        }
3353                    });
3354                    quote! {
3355                        let part = #part;
3356                        #content_type
3357                        form = form.part(#wire_name, part);
3358                    }
3359                }
3360            };
3361            let add_value = if nullable_items {
3362                quote! {
3363                    for value in value.iter().flatten() {
3364                        #add_item
3365                    }
3366                }
3367            } else if is_array {
3368                quote! {
3369                    for value in value {
3370                        #add_item
3371                    }
3372                }
3373            } else {
3374                add_item
3375            };
3376            parts.push(if is_tri_state {
3377                // Multipart has no representation for a JSON null part. A
3378                // present concrete value is sent; both outer absence and an
3379                // explicit-null model state omit the part.
3380                quote! {
3381                    if let Some(Some(value)) = &request.#ident {
3382                        #add_value
3383                    }
3384                }
3385            } else if field.is_required && is_nullable {
3386                quote! {
3387                    if let Some(value) = &request.#ident {
3388                        #add_value
3389                    }
3390                }
3391            } else if field.is_required {
3392                quote! {
3393                    let value = &request.#ident;
3394                    #add_value
3395                }
3396            } else {
3397                quote! {
3398                    if let Some(value) = &request.#ident {
3399                        #add_value
3400                    }
3401                }
3402            });
3403        }
3404
3405        quote! {
3406            let mut form = reqwest::multipart::Form::new();
3407            #(#parts)*
3408            req = req.multipart(form);
3409        }
3410    }
3411
3412    /// Generate request body serialization based on content type
3413    /// Emit statements that mutate `req` to apply the request body. Returns
3414    /// explicit zero-length framing for bodyless POST, PUT, and PATCH requests.
3415    /// Optional bodies (T11) gate the application on `Some(_)`; required bodies
3416    /// apply unconditionally.
3417    fn generate_request_body(&self, op: &OperationInfo, analysis: &SchemaAnalysis) -> TokenStream {
3418        let empty_request_framing = Self::generate_empty_request_framing(op);
3419        let Some(rb) = op.request_body.as_ref() else {
3420            return empty_request_framing;
3421        };
3422        use crate::analysis::RequestBodyContent;
3423        let required = op.request_body_required;
3424        let (ident, apply): (TokenStream, TokenStream) = match rb {
3425            RequestBodyContent::Json { media_type, .. } => (
3426                quote! { request },
3427                quote! {
3428                    req = req
3429                        .body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
3430                        .header("content-type", #media_type);
3431                },
3432            ),
3433            RequestBodyContent::FormUrlEncoded { media_type, .. } => (
3434                quote! { request },
3435                quote! {
3436                    req = req
3437                        .body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
3438                        .header("content-type", #media_type);
3439                },
3440            ),
3441            RequestBodyContent::Multipart {
3442                schema_name,
3443                encoding,
3444                ..
3445            } => (
3446                quote! { request },
3447                self.generate_typed_multipart_form(schema_name, encoding, analysis),
3448            ),
3449            RequestBodyContent::OctetStream { media_type } => (
3450                quote! { body },
3451                quote! {
3452                    req = req
3453                        .body(body)
3454                        .header("content-type", #media_type);
3455                },
3456            ),
3457            RequestBodyContent::Binary { media_type } => (
3458                quote! { body },
3459                quote! {
3460                    req = req
3461                        .body(body)
3462                        .header("content-type", #media_type);
3463                },
3464            ),
3465            RequestBodyContent::TextPlain { media_type } => (
3466                quote! { body },
3467                quote! {
3468                    req = req
3469                        .body(body)
3470                        .header("content-type", #media_type);
3471                },
3472            ),
3473            RequestBodyContent::Unsupported { media_types } => {
3474                let media_type = media_types
3475                    .iter()
3476                    .find(|media_type| !crate::openapi::is_wildcard_media_type(media_type))
3477                    .map(String::as_str);
3478                let Some(media_type) = media_type else {
3479                    let ranges = media_types.join(", ");
3480                    let message = format!(
3481                        "request body for operation `{}` declares only wildcard media ranges ({ranges}); a concrete Content-Type is required",
3482                        op.operation_id,
3483                    );
3484                    return if required {
3485                        quote! {
3486                            let _ = body;
3487                            return Err(HttpError::Config(#message.to_string()).into());
3488                        }
3489                    } else {
3490                        quote! {
3491                            if body.is_some() {
3492                                return Err(HttpError::Config(#message.to_string()).into());
3493                            }
3494                            #empty_request_framing
3495                        }
3496                    };
3497                };
3498                (
3499                    quote! { body },
3500                    quote! {
3501                        req = req
3502                            .body(body)
3503                            .header("content-type", #media_type);
3504                    },
3505                )
3506            }
3507            RequestBodyContent::SchemaLess { .. } => return empty_request_framing,
3508        };
3509        if required {
3510            apply
3511        } else {
3512            quote! {
3513                if let Some(#ident) = #ident {
3514                    #apply
3515                } else {
3516                    #empty_request_framing
3517                }
3518            }
3519        }
3520    }
3521
3522    /// Emit explicit HTTP/1.1 framing when an operation sends no request body.
3523    /// RFC 9110 recommends `Content-Length: 0` for methods that define request
3524    /// content semantics. Methods without those semantics intentionally remain
3525    /// unchanged.
3526    fn generate_empty_request_framing(op: &OperationInfo) -> TokenStream {
3527        if ["POST", "PUT", "PATCH"]
3528            .iter()
3529            .any(|method| op.method.eq_ignore_ascii_case(method))
3530        {
3531            quote! {
3532                req = req.header(reqwest::header::CONTENT_LENGTH, "0");
3533            }
3534        } else {
3535            quote! {}
3536        }
3537    }
3538
3539    /// Find the success (2xx) response schema name, if any.
3540    ///
3541    /// Only considers 2xx status codes. Error schemas (4xx, 5xx) are ignored
3542    /// so that endpoints like 204 No Content correctly return `()` instead of
3543    /// accidentally picking up the error schema (e.g. `BadRequestError`).
3544    fn get_success_response_schema<'a>(
3545        &self,
3546        op: &'a OperationInfo,
3547    ) -> Option<(&'a str, &'a String)> {
3548        op.response_schemas
3549            .get_key_value("200")
3550            .or_else(|| op.response_schemas.get_key_value("201"))
3551            .or_else(|| {
3552                op.response_schemas
3553                    .iter()
3554                    .find(|(code, _)| code.starts_with('2'))
3555            })
3556            .map(|(status, schema)| (status.as_str(), schema))
3557    }
3558
3559    fn get_success_response<'a>(
3560        &self,
3561        analysis: &'a SchemaAnalysis,
3562        op: &'a OperationInfo,
3563    ) -> ClientSuccessSelection<'a> {
3564        if let Some(responses) = analysis.operation_responses.get(&op.operation_id) {
3565            let mut candidates = Vec::new();
3566            for preferred in ["200", "201"] {
3567                if let Some((status, response)) = responses.get_key_value(preferred) {
3568                    candidates.push((status.as_str(), response));
3569                }
3570            }
3571            candidates.extend(
3572                responses
3573                    .iter()
3574                    .filter(|(status, _)| {
3575                        status.starts_with('2')
3576                            && status.as_str() != "200"
3577                            && status.as_str() != "201"
3578                    })
3579                    .map(|(status, response)| (status.as_str(), response)),
3580            );
3581
3582            let selected = candidates
3583                .iter()
3584                .copied()
3585                .find(|(_, response)| {
3586                    matches!(
3587                        Self::response_body(response),
3588                        ClientSuccessBody::Json(_)
3589                            | ClientSuccessBody::Text
3590                            | ClientSuccessBody::Binary
3591                    )
3592                })
3593                .or_else(|| {
3594                    candidates.iter().copied().find(|(_, response)| {
3595                        matches!(
3596                            Self::response_body(response),
3597                            ClientSuccessBody::EventStream
3598                        )
3599                    })
3600                })
3601                .or_else(|| candidates.first().copied());
3602
3603            if let Some((_, response)) = selected {
3604                let body = Self::response_body(response);
3605                let statuses: Vec<&str> = candidates
3606                    .iter()
3607                    .filter_map(|(status, candidate)| {
3608                        Self::success_bodies_are_compatible(body, Self::response_body(candidate))
3609                            .then_some(*status)
3610                    })
3611                    .collect();
3612                let accept = match &response.body {
3613                    Some(OperationResponseBody::Json { media_type, .. })
3614                    | Some(OperationResponseBody::Text { media_type }) => Some(media_type.as_str()),
3615                    Some(OperationResponseBody::Binary {
3616                        media_type,
3617                        wildcard,
3618                    }) => (!wildcard).then_some(media_type.as_str()),
3619                    None if response.schema_name.is_some() => {
3620                        response.media_type.as_deref().or(Some("application/json"))
3621                    }
3622                    None if response.supports_streaming => Some("text/event-stream"),
3623                    None => None,
3624                };
3625                let excluded_statuses = responses
3626                    .keys()
3627                    .filter(|status| {
3628                        status.len() == 3
3629                            && status.bytes().all(|c| c.is_ascii_digit())
3630                            && !statuses.contains(&status.as_str())
3631                    })
3632                    .map(String::as_str)
3633                    .collect();
3634                return ClientSuccessSelection {
3635                    statuses,
3636                    excluded_statuses,
3637                    body,
3638                    accept,
3639                };
3640            }
3641        }
3642
3643        if let Some((_status, schema_name)) = self.get_success_response_schema(op) {
3644            let statuses = op
3645                .response_schemas
3646                .iter()
3647                .filter_map(|(candidate_status, candidate_schema)| {
3648                    (candidate_status.starts_with('2') && candidate_schema == schema_name)
3649                        .then_some(candidate_status.as_str())
3650                })
3651                .collect();
3652            ClientSuccessSelection {
3653                statuses,
3654                excluded_statuses: Vec::new(),
3655                body: ClientSuccessBody::Json(schema_name),
3656                accept: Some("application/json"),
3657            }
3658        } else if Self::returns_raw_event_stream(op) {
3659            ClientSuccessSelection {
3660                statuses: Vec::new(),
3661                excluded_statuses: Vec::new(),
3662                body: ClientSuccessBody::EventStream,
3663                accept: Some("text/event-stream"),
3664            }
3665        } else {
3666            ClientSuccessSelection {
3667                statuses: Vec::new(),
3668                excluded_statuses: Vec::new(),
3669                body: ClientSuccessBody::Empty,
3670                accept: None,
3671            }
3672        }
3673    }
3674
3675    fn response_body(response: &crate::analysis::OperationResponse) -> ClientSuccessBody<'_> {
3676        match &response.body {
3677            Some(OperationResponseBody::Json { schema_name, .. }) => {
3678                ClientSuccessBody::Json(schema_name)
3679            }
3680            Some(OperationResponseBody::Text { .. }) => ClientSuccessBody::Text,
3681            Some(OperationResponseBody::Binary { .. }) => ClientSuccessBody::Binary,
3682            None if response.schema_name.is_some() => {
3683                ClientSuccessBody::Json(response.schema_name.as_deref().unwrap_or_default())
3684            }
3685            None if response.supports_streaming => ClientSuccessBody::EventStream,
3686            None => ClientSuccessBody::Empty,
3687        }
3688    }
3689
3690    fn success_bodies_are_compatible(
3691        selected: ClientSuccessBody<'_>,
3692        candidate: ClientSuccessBody<'_>,
3693    ) -> bool {
3694        match (selected, candidate) {
3695            (ClientSuccessBody::Json(selected), ClientSuccessBody::Json(candidate)) => {
3696                selected == candidate
3697            }
3698            (ClientSuccessBody::Text, ClientSuccessBody::Text)
3699            | (ClientSuccessBody::Binary, ClientSuccessBody::Binary)
3700            | (ClientSuccessBody::EventStream, ClientSuccessBody::EventStream)
3701            | (ClientSuccessBody::Empty, ClientSuccessBody::Empty) => true,
3702            _ => false,
3703        }
3704    }
3705
3706    fn response_type_for_body(&self, body: ClientSuccessBody<'_>) -> TokenStream {
3707        match body {
3708            ClientSuccessBody::Json(response_type) => {
3709                // Convert schema name to Rust type name (handles underscores, etc.)
3710                let rust_type_name = self.to_rust_type_name(response_type);
3711                let response_ident =
3712                    syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
3713                quote! { #response_ident }
3714            }
3715            ClientSuccessBody::Text => quote! { String },
3716            ClientSuccessBody::Binary => quote! { bytes::Bytes },
3717            ClientSuccessBody::EventStream => {
3718                quote! { impl futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> }
3719            }
3720            ClientSuccessBody::ParsedSse => {
3721                quote! { super::sse::BoxSseStream<Result<super::sse::SseEvent<String>, super::sse::StreamingError>> }
3722            }
3723            ClientSuccessBody::Empty => quote! { () },
3724        }
3725    }
3726
3727    fn success_status_guard(statuses: &[&str]) -> TokenStream {
3728        if statuses.is_empty() {
3729            return quote! { status.is_success() };
3730        }
3731        let guards = statuses
3732            .iter()
3733            .map(|status| Self::single_status_guard(status));
3734        quote! { false #( || #guards )* }
3735    }
3736
3737    fn success_selection_guard(
3738        &self,
3739        _operation: &OperationInfo,
3740        success: &ClientSuccessSelection<'_>,
3741    ) -> TokenStream {
3742        let guard = Self::success_status_guard(&success.statuses);
3743        if !success
3744            .statuses
3745            .iter()
3746            .any(|status| status.eq_ignore_ascii_case("2XX"))
3747        {
3748            return guard;
3749        }
3750        let exact: Vec<_> = success
3751            .excluded_statuses
3752            .iter()
3753            .map(|status| Self::single_status_guard(status))
3754            .collect();
3755        quote! { (#guard) && !(false #( || #exact )*) }
3756    }
3757
3758    fn single_status_guard(status: &str) -> TokenStream {
3759        match status {
3760            status if status.chars().all(|character| character.is_ascii_digit()) => {
3761                let status: u16 = status.parse().unwrap_or_default();
3762                quote! { status_code == #status }
3763            }
3764            status if matches!(status.as_bytes(), [b'1'..=b'5', b'X' | b'x', b'X' | b'x']) => {
3765                let class = u16::from(status.as_bytes()[0] - b'0');
3766                quote! { status_code / 100 == #class }
3767            }
3768            _ => quote! { status.is_success() },
3769        }
3770    }
3771
3772    /// True when the operation's success response carries `text/event-stream`
3773    /// and nothing this generator can model as a JSON body.
3774    ///
3775    /// These previously generated `-> Result<(), _>` and then called
3776    /// `response.text().await`, which on a live SSE stream never returns: the
3777    /// caller's task deadlocks rather than erroring (openapi-generator-x9v).
3778    /// The streaming signal was already detected in analysis and honored by the
3779    /// server generator; only the client ignored it.
3780    ///
3781    /// Operations declaring *both* a JSON body and `text/event-stream` keep
3782    /// their JSON contract here — those are the `stream: true` style endpoints
3783    /// covered by the explicit `[streaming]` configuration, and silently
3784    /// changing their return type would break existing callers.
3785    fn returns_raw_event_stream(op: &OperationInfo) -> bool {
3786        op.supports_streaming
3787    }
3788
3789    /// Generate error handling.
3790    ///
3791    /// Buffers non-streaming responses once, retaining both exact bytes and a
3792    /// lossy UTF-8 view for compatibility. Only the selected declared success
3793    /// status is parsed into the generated return type; other 2xx statuses are
3794    /// inspectable `ApiError`s rather than being fed to an incompatible parser.
3795    fn generate_error_handling(
3796        &self,
3797        op: &OperationInfo,
3798        success: ClientSuccessSelection<'_>,
3799    ) -> TokenStream {
3800        let op_error_type = self.op_error_type_token(op);
3801        let success_body = success.body;
3802        let success_status_guard = self.success_selection_guard(op, &success);
3803        let selected_status = if success.statuses.is_empty() {
3804            "any declared 2xx response".to_string()
3805        } else {
3806            success.statuses.join(", ")
3807        };
3808        // With no 2xx status declared, the success guard is
3809        // `status.is_success()` itself, so no successful status can reach the
3810        // branch that rejects unexpected ones.
3811        let rejects_unexpected_success = !success.statuses.is_empty();
3812
3813        let success_branch = match success_body {
3814            ClientSuccessBody::Json(_) => quote! {
3815                match serde_json::from_str(&body_text) {
3816                    Ok(body) => Ok(body),
3817                    Err(e) => Err(ApiOpError::Api(ApiError {
3818                        status: status_code,
3819                        headers: headers,
3820                        body: body_text,
3821                        raw_body,
3822                        typed: None,
3823                        parse_error: Some(format!(
3824                            "failed to deserialize 2xx response body: {}",
3825                            e
3826                        )),
3827                    })),
3828                }
3829            },
3830            ClientSuccessBody::Text => quote! {
3831                let _ = raw_body;
3832                Ok(body_text)
3833            },
3834            ClientSuccessBody::Empty => quote! {
3835                let _ = body_text;
3836                let _ = raw_body;
3837                let _ = headers;
3838                Ok(())
3839            },
3840            ClientSuccessBody::Binary
3841            | ClientSuccessBody::EventStream
3842            | ClientSuccessBody::ParsedSse => quote! {},
3843        };
3844
3845        let error_match_arms = self.generate_error_match_arms(op);
3846
3847        // Streaming success path: hand back the live byte stream instead of
3848        // buffering it. Reading an SSE body to a string blocks until the server
3849        // closes the connection, which is precisely what it will not do.
3850        // The error path still buffers — an error response is finite.
3851        if matches!(
3852            success_body,
3853            ClientSuccessBody::EventStream | ClientSuccessBody::ParsedSse
3854        ) {
3855            let stream = if matches!(success_body, ClientSuccessBody::ParsedSse) {
3856                quote! { super::sse::parse_sse_response(response) }
3857            } else {
3858                quote! { response.bytes_stream() }
3859            };
3860            let reject_unexpected_success = if rejects_unexpected_success {
3861                quote! {
3862                    if status.is_success() {
3863                        return Err(ApiOpError::Api(ApiError {
3864                            status: status_code,
3865                            headers,
3866                            body: String::new(),
3867                            raw_body: Vec::new(),
3868                            typed: None,
3869                            parse_error: Some(format!(
3870                                "unexpected successful status {}; generated return type selects `{}`; live response body was not buffered",
3871                                status_code,
3872                                #selected_status,
3873                            )),
3874                        }));
3875                    }
3876                }
3877            } else {
3878                quote! {}
3879            };
3880            return quote! {
3881                let status = response.status();
3882                let status_code = status.as_u16();
3883                let headers = response.headers().clone();
3884
3885                if #success_status_guard {
3886                    Ok(#stream)
3887                } else {
3888                    #reject_unexpected_success
3889                    let body_bytes = __read_bounded_response_body(
3890                        response,
3891                        self.max_response_body_bytes,
3892                    ).await?;
3893                    let raw_body = body_bytes;
3894                    let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3895                    let typed: Option<#op_error_type>;
3896                    let parse_error: Option<String>;
3897                    #error_match_arms
3898                    Err(ApiOpError::Api(ApiError {
3899                        status: status_code,
3900                        headers,
3901                        body: body_text,
3902                        raw_body,
3903                        typed,
3904                        parse_error,
3905                    }))
3906                }
3907            };
3908        }
3909
3910        if matches!(success_body, ClientSuccessBody::Binary) {
3911            let reject_unexpected_success = if rejects_unexpected_success {
3912                quote! {
3913                    if status.is_success() {
3914                        return Err(ApiOpError::Api(ApiError {
3915                            status: status_code,
3916                            headers,
3917                            body: body_text,
3918                            raw_body,
3919                            typed: None,
3920                            parse_error: Some(format!(
3921                                "unexpected successful status {}; generated return type selects `{}`",
3922                                status_code,
3923                                #selected_status,
3924                            )),
3925                        }));
3926                    }
3927                }
3928            } else {
3929                quote! {}
3930            };
3931            return quote! {
3932                let status = response.status();
3933                let status_code = status.as_u16();
3934                let headers = response.headers().clone();
3935
3936                let body_bytes = __read_bounded_response_body(
3937                    response,
3938                    self.max_response_body_bytes,
3939                ).await?;
3940                if #success_status_guard {
3941                    Ok(bytes::Bytes::from(body_bytes))
3942                } else {
3943                    let raw_body = body_bytes;
3944                    let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3945                    #reject_unexpected_success
3946                    let typed: Option<#op_error_type>;
3947                    let parse_error: Option<String>;
3948                    #error_match_arms
3949                    Err(ApiOpError::Api(ApiError {
3950                        status: status_code,
3951                        headers,
3952                        body: body_text,
3953                        raw_body,
3954                        typed,
3955                        parse_error,
3956                    }))
3957                }
3958            };
3959        }
3960
3961        let reject_unexpected_success = if rejects_unexpected_success {
3962            quote! {
3963                else if status.is_success() {
3964                    Err(ApiOpError::Api(ApiError {
3965                        status: status_code,
3966                        headers,
3967                        body: body_text,
3968                        raw_body,
3969                        typed: None,
3970                        parse_error: Some(format!(
3971                            "unexpected successful status {}; generated return type selects `{}`",
3972                            status_code,
3973                            #selected_status,
3974                        )),
3975                    }))
3976                }
3977            }
3978        } else {
3979            quote! {}
3980        };
3981
3982        quote! {
3983            let status = response.status();
3984            let status_code = status.as_u16();
3985            let headers = response.headers().clone();
3986            let body_bytes = __read_bounded_response_body(
3987                response,
3988                self.max_response_body_bytes,
3989            ).await?;
3990            let raw_body = body_bytes;
3991            let body_text = String::from_utf8_lossy(&raw_body).into_owned();
3992
3993            if #success_status_guard {
3994                #success_branch
3995            } #reject_unexpected_success else {
3996                let typed: Option<#op_error_type>;
3997                let parse_error: Option<String>;
3998                #error_match_arms
3999                Err(ApiOpError::Api(ApiError {
4000                    status: status_code,
4001                    headers,
4002                    body: body_text,
4003                    raw_body,
4004                    typed,
4005                    parse_error,
4006                }))
4007            }
4008        }
4009    }
4010
4011    /// Generate the match arms that select which per-op error variant to
4012    /// deserialize the response body into based on the runtime status code.
4013    fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
4014        let arms: Vec<TokenStream> = op
4015            .response_schemas
4016            .iter()
4017            .filter(|(code, _)| !code.starts_with('2'))
4018            .filter_map(|(code, schema)| {
4019                let variant_ident = Self::op_error_variant_ident(code);
4020                let payload_ty_name = self.to_rust_type_name(schema);
4021                let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
4022                let enum_ident = self.op_error_enum_ident(op);
4023
4024                // T8: range-keyed responses (1XX/2XX/3XX/4XX/5XX) per OAS
4025                // 3.x §"Responses Object". Specific codes still take priority
4026                // (handled by ordering — concrete codes deserialize first
4027                // because the generic dispatch is a generic `_ if (range)`).
4028                let pattern = match code.as_str() {
4029                    "default" | "Default" => return None, // handled in fallback
4030                    other if other.chars().all(|c| c.is_ascii_digit()) => {
4031                        let n: u16 = other.parse().ok()?;
4032                        quote! { #n }
4033                    }
4034                    "1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
4035                    "2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
4036                    "3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
4037                    "4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
4038                    "5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
4039                    _ => return None,
4040                };
4041
4042                Some(quote! {
4043                    #pattern => {
4044                        match serde_json::from_str::<#payload_ty>(&body_text) {
4045                            Ok(v) => {
4046                                typed = Some(#enum_ident::#variant_ident(v));
4047                                parse_error = None;
4048                            }
4049                            Err(e) => {
4050                                typed = None;
4051                                parse_error = Some(e.to_string());
4052                            }
4053                        }
4054                    }
4055                })
4056            })
4057            .collect();
4058
4059        // Fallback for "default" or undeclared status codes: try to parse
4060        // as `serde_json::Value` for inspectability when the op's error
4061        // type is generic, otherwise leave typed = None.
4062        // Must mirror op_error_type_token: if op_error_type is the typed
4063        // enum (any non-2xx response, including `default`), the fallback arm
4064        // can't deserialize into `serde_json::Value` because `typed` is the
4065        // enum. Default to `typed = None` in that case.
4066        let has_typed_enum = op
4067            .response_schemas
4068            .iter()
4069            .any(|(code, _)| !code.starts_with('2'));
4070
4071        // A spec-declared `default` response is the catch-all arm's payload
4072        // type. Without this the generated enum carries a `Default(..)` variant
4073        // that nothing ever constructs, so a response matched only by `default`
4074        // — a perfectly parseable typed body — still surfaces as
4075        // `typed: None` and callers fall back to raw strings
4076        // (openapi-generator-nu7).
4077        let default_payload = op
4078            .response_schemas
4079            .iter()
4080            .find(|(code, _)| matches!(code.as_str(), "default" | "Default"))
4081            .map(|(_, schema)| self.to_rust_type_name(schema));
4082
4083        let default_arm = if let Some(payload_ty_name) = default_payload {
4084            let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
4085            let enum_ident = self.op_error_enum_ident(op);
4086            quote! {
4087                _ => {
4088                    match serde_json::from_str::<#payload_ty>(&body_text) {
4089                        Ok(v) => {
4090                            typed = Some(#enum_ident::Default(v));
4091                            parse_error = None;
4092                        }
4093                        Err(e) => {
4094                            typed = None;
4095                            parse_error = Some(e.to_string());
4096                        }
4097                    }
4098                }
4099            }
4100        } else if has_typed_enum {
4101            quote! {
4102                _ => {
4103                    typed = None;
4104                    parse_error = None;
4105                }
4106            }
4107        } else {
4108            // No typed enum — op_error_type is serde_json::Value.
4109            quote! {
4110                _ => {
4111                    match serde_json::from_str::<serde_json::Value>(&body_text) {
4112                        Ok(v) => {
4113                            typed = Some(v);
4114                            parse_error = None;
4115                        }
4116                        Err(e) => {
4117                            typed = None;
4118                            parse_error = Some(e.to_string());
4119                        }
4120                    }
4121                }
4122            }
4123        };
4124
4125        if arms.is_empty() {
4126            // No declared status arms — just the fallback.
4127            quote! {
4128                match status_code {
4129                    #default_arm
4130                }
4131            }
4132        } else {
4133            quote! {
4134                match status_code {
4135                    #(#arms)*
4136                    #default_arm
4137                }
4138            }
4139        }
4140    }
4141
4142    /// Generate URL construction with path parameter substitution
4143    fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
4144        // Check if path has parameters (contains {...})
4145        if path.contains('{') {
4146            self.generate_url_with_params(path, op)
4147        } else {
4148            quote! {
4149                let request_url = format!("{}{}", self.base_url, #path);
4150            }
4151        }
4152    }
4153
4154    /// Generate URL with path parameters
4155    fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
4156        // Find all path parameters in the operation.
4157        let path_params: Vec<_> = op
4158            .parameters
4159            .iter()
4160            .filter(|p| p.location == "path")
4161            .collect();
4162
4163        // T5: percent-encode each path-template variable per RFC3986 §3.3.
4164        // We build a positional-arg format string by walking the template
4165        // left-to-right and emitting one `{}` + one format arg per
4166        // placeholder occurrence. Cloudflare has paths like
4167        // `/accounts/{account_id}/.../accounts/{account_id}` — the same
4168        // variable appears twice. A naive `replace_all` produced two `{}`
4169        // placeholders but only one format arg (E0277). Per-occurrence
4170        // emission keeps them in sync.
4171        let mut format_string = String::with_capacity(path.len());
4172        let mut format_args: Vec<TokenStream> = Vec::new();
4173        let mut chars = path.chars().peekable();
4174        while let Some(c) = chars.next() {
4175            if c != '{' {
4176                format_string.push(c);
4177                continue;
4178            }
4179            // Read until the matching '}'.
4180            let mut name = String::new();
4181            while let Some(&n) = chars.peek() {
4182                chars.next();
4183                if n == '}' {
4184                    break;
4185                }
4186                name.push(n);
4187            }
4188            // Resolve to a path param. If no match, leave the placeholder
4189            // verbatim (real-world spec bug — this op shouldn't have made
4190            // it past analysis).
4191            let param = path_params.iter().find(|p| p.name == name);
4192            let Some(param) = param else {
4193                format_string.push('{');
4194                format_string.push_str(&name);
4195                format_string.push('}');
4196                continue;
4197            };
4198            format_string.push_str("{}");
4199            let param_name_snake = self.operation_param_ident(op, param);
4200            let param_ident = Self::to_field_ident(&param_name_snake);
4201            let encoder = if param.allow_reserved {
4202                quote! { __pct_encode_path_reserved }
4203            } else {
4204                quote! { __pct_encode_path_segment }
4205            };
4206            if Self::param_uses_as_ref_str(param) {
4207                format_args.push(quote! {
4208                    #encoder(#param_ident.as_ref())
4209                });
4210            } else {
4211                format_args.push(quote! {
4212                    #encoder(&#param_ident.to_string())
4213                });
4214            }
4215        }
4216
4217        if format_args.is_empty() {
4218            quote! {
4219                let request_url = format!("{}{}", self.base_url, #path);
4220            }
4221        } else {
4222            quote! {
4223                let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
4224            }
4225        }
4226    }
4227
4228    /// Resolve the Rust ident for a parameter. Prefers the disambiguated
4229    /// `rust_ident` set by the analyzer (which dedupes across the whole
4230    /// operation), falling back to a fresh sanitize of the wire name when
4231    /// no analyzer-side ident is present.
4232    pub(crate) fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
4233        if let Some(ident) = &param.rust_ident {
4234            // Apply the keyword-escape and self/super/crate dance the
4235            // sanitize fn does. The analyzer's base ident is already the
4236            // snake/kebab-aware shape; we only need post-processing.
4237            return self.escape_keyword_ident(ident);
4238        }
4239        self.sanitize_param_name(&param.name)
4240    }
4241
4242    fn escape_keyword_ident(&self, snake_case: &str) -> String {
4243        if matches!(snake_case, "self" | "super" | "crate" | "Self") {
4244            return format!("{snake_case}_param");
4245        }
4246        if Self::is_rust_keyword(snake_case) {
4247            format!("r#{snake_case}")
4248        } else {
4249            snake_case.to_string()
4250        }
4251    }
4252
4253    /// Sanitize a parameter name by escaping Rust reserved keywords with raw
4254    /// identifiers and disambiguating Twilio-style suffix operators
4255    /// (`StartTime`, `StartTime<`, `StartTime>` would otherwise all snake-
4256    /// case to `start_time`).
4257    fn sanitize_param_name(&self, name: &str) -> String {
4258        // Disambiguate before stripping. `<`, `>`, `<=`, `>=` are common in
4259        // filter-style query params; map them to `_lt` / `_gt` etc. so the
4260        // Rust ident is unique while the wire-level param name stays the
4261        // original string elsewhere in the codegen.
4262        let suffix = if name.ends_with("<=") {
4263            "_lte"
4264        } else if name.ends_with(">=") {
4265            "_gte"
4266        } else if name.ends_with('<') {
4267            "_lt"
4268        } else if name.ends_with('>') {
4269            "_gt"
4270        } else {
4271            ""
4272        };
4273        let stripped = name.trim_end_matches(['<', '>', '=']);
4274        let mut snake_case = stripped.to_snake_case();
4275        if snake_case.is_empty() {
4276            snake_case.push_str("parameter");
4277        } else if snake_case.starts_with(|character: char| character.is_ascii_digit()) {
4278            snake_case.insert(0, '_');
4279        }
4280        snake_case.push_str(suffix);
4281
4282        if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
4283            return format!("{snake_case}_param");
4284        }
4285        if Self::is_rust_keyword(&snake_case) {
4286            format!("r#{snake_case}")
4287        } else {
4288            snake_case
4289        }
4290    }
4291}