Skip to main content

openapi_to_rust/
client_generator.rs

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