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