Skip to main content

openapi_to_rust/server/
codegen.rs

1//! Server codegen — trait + typed response enums (P4).
2//!
3//! Emits one trait per tag (or a `ServerApi` trait for untagged
4//! operations) plus a per-operation response enum with an
5//! `IntoResponse` impl that maps each variant to its documented
6//! status code.
7//!
8//! Router wiring, extractors, and SSE response variants are P5.
9
10use crate::analysis::{
11    ObjectAdditionalProperties, OperationInfo, OperationResponse, OperationResponseBody,
12    ParameterInfo, QuerySerialization, RequestBodyContent, SchemaAnalysis, SchemaType,
13};
14use crate::config::ServerSection;
15use crate::generator::{CodeGenerator, GeneratedFile, GeneratorConfig, rust_type_name};
16
17use super::{OperationIndex, Selector};
18use heck::{ToPascalCase, ToSnakeCase};
19use proc_macro2::TokenStream;
20use quote::{format_ident, quote};
21use std::collections::BTreeMap;
22use std::path::PathBuf;
23
24#[derive(Clone, Copy)]
25enum MultipartFieldKind {
26    Binary,
27    String,
28    Integer,
29    UnsignedInteger,
30    Number,
31    Boolean,
32}
33
34struct MultipartFieldPlan {
35    wire_name: String,
36    field_ident: syn::Ident,
37    required: bool,
38    kind: MultipartFieldKind,
39}
40
41/// Compute the set of schema names transitively reachable from the
42/// request/response/parameter shapes of the given operations.
43///
44/// Used by client/server model pruning to drop unreferenced types from
45/// `types.rs`. Walks every `$ref` in each schema's raw JSON
46/// (`AnalyzedSchema.original`) rather than the analyzer's
47/// `dependencies` field — the latter is incomplete for some
48/// schemas (e.g. struct fields whose target schemas weren't
49/// individually tracked).
50///
51/// Inline parameter enums (whose `rust_type` is a synthetic name
52/// without a matching `analysis.schemas` entry) are not the
53/// responsibility of this walk — they're emitted directly by the
54/// server codegen from `parameter.enum_values`.
55pub fn reachable_schemas(
56    analysis: &SchemaAnalysis,
57    ops: &[&OperationInfo],
58) -> std::collections::BTreeSet<String> {
59    reachable_schemas_with_roots(analysis, ops, &[])
60}
61
62/// [`reachable_schemas`] plus explicit schema roots used by configured
63/// consumers such as SSE event-union types.
64pub fn reachable_schemas_with_roots(
65    analysis: &SchemaAnalysis,
66    ops: &[&OperationInfo],
67    extra_roots: &[String],
68) -> std::collections::BTreeSet<String> {
69    let mut keep: std::collections::BTreeSet<String> = Default::default();
70    let mut queue: Vec<String> = Vec::new();
71
72    let seed =
73        |name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
74            if !name.is_empty() && keep.insert(name.to_string()) {
75                queue.push(name.to_string());
76            }
77        };
78
79    for op in ops {
80        if let Some(rb) = &op.request_body
81            && let Some(name) = rb.schema_name()
82        {
83            seed(name, &mut queue, &mut keep);
84        }
85        for ty in op.response_schemas.values() {
86            seed(ty, &mut queue, &mut keep);
87        }
88        for p in &op.parameters {
89            if let Some(name) = &p.schema_ref {
90                seed(name, &mut queue, &mut keep);
91            }
92            if let Some(
93                QuerySerialization::FormExplodedArray {
94                    item_type: crate::analysis::ArrayItemType::SchemaRef(name),
95                }
96                | QuerySerialization::FormArray {
97                    item_type: crate::analysis::ArrayItemType::SchemaRef(name),
98                }
99                | QuerySerialization::SimpleHeaderArray {
100                    item_type: crate::analysis::ArrayItemType::SchemaRef(name),
101                },
102            ) = &p.query_serialization
103            {
104                seed(name, &mut queue, &mut keep);
105            }
106            if let Some(
107                QuerySerialization::FormExplodedArray {
108                    item_type:
109                        crate::analysis::ArrayItemType::FlatStructRef { schema_name, .. }
110                        | crate::analysis::ArrayItemType::NestedStructRef { schema_name, .. },
111                }
112                | QuerySerialization::FormArray {
113                    item_type:
114                        crate::analysis::ArrayItemType::FlatStructRef { schema_name, .. }
115                        | crate::analysis::ArrayItemType::NestedStructRef { schema_name, .. },
116                }
117                | QuerySerialization::SimpleHeaderArray {
118                    item_type:
119                        crate::analysis::ArrayItemType::FlatStructRef { schema_name, .. }
120                        | crate::analysis::ArrayItemType::NestedStructRef { schema_name, .. },
121                },
122            ) = &p.query_serialization
123            {
124                seed(schema_name, &mut queue, &mut keep);
125            }
126        }
127    }
128    for root in extra_roots {
129        seed(root, &mut queue, &mut keep);
130    }
131
132    while let Some(name) = queue.pop() {
133        if let Some(schema) = analysis.schemas.get(&name) {
134            // Walk the raw JSON for every `$ref` string and feed
135            // the referenced schema names back into the queue.
136            collect_refs(&schema.original, &mut queue, &mut keep);
137            // Belt-and-braces: also include the analyzer's tracked
138            // dependencies, which sometimes catch refs that live
139            // outside the immediate JSON tree (e.g. allOf compositions
140            // resolved before the snapshot was captured).
141            for dep in &schema.dependencies {
142                seed(dep, &mut queue, &mut keep);
143            }
144            // The analyzed shape is the authoritative generated type graph.
145            // It includes ownership edges for inline/synthetic schemas that
146            // do not appear as `$ref`s in the source document.
147            collect_schema_type_refs(&schema.schema_type, &mut queue, &mut keep);
148        }
149    }
150
151    keep
152}
153
154fn collect_schema_type_refs(
155    schema_type: &SchemaType,
156    queue: &mut Vec<String>,
157    keep: &mut std::collections::BTreeSet<String>,
158) {
159    let seed =
160        |name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
161            if !name.is_empty() && keep.insert(name.to_string()) {
162                queue.push(name.to_string());
163            }
164        };
165
166    match schema_type {
167        SchemaType::Primitive { .. }
168        | SchemaType::StringEnum { .. }
169        | SchemaType::ExtensibleEnum { .. } => {}
170        SchemaType::Object {
171            properties,
172            additional_properties,
173            ..
174        } => {
175            for property in properties.values() {
176                collect_schema_type_refs(&property.schema_type, queue, keep);
177            }
178            if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
179                collect_schema_type_refs(value_type, queue, keep);
180            }
181        }
182        SchemaType::DiscriminatedUnion { variants, .. } => {
183            for variant in variants {
184                seed(&variant.type_name, queue, keep);
185            }
186        }
187        SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
188            for variant in variants {
189                seed(&variant.target, queue, keep);
190            }
191        }
192        SchemaType::Array { item_type } => collect_schema_type_refs(item_type, queue, keep),
193        SchemaType::Nullable { inner_type } => collect_schema_type_refs(inner_type, queue, keep),
194        SchemaType::Tuple { element_types } => {
195            for element_type in element_types {
196                collect_schema_type_refs(element_type, queue, keep);
197            }
198        }
199        SchemaType::Reference { target } => seed(target, queue, keep),
200        SchemaType::Untyped { .. } => {}
201    }
202}
203
204fn collect_refs(
205    value: &serde_json::Value,
206    queue: &mut Vec<String>,
207    keep: &mut std::collections::BTreeSet<String>,
208) {
209    match value {
210        serde_json::Value::Object(map) => {
211            for (k, v) in map {
212                if k == "$ref"
213                    && let Some(s) = v.as_str()
214                    && let Some(name) = s.strip_prefix("#/components/schemas/")
215                    && keep.insert(name.to_string())
216                {
217                    queue.push(name.to_string());
218                }
219                collect_refs(v, queue, keep);
220            }
221        }
222        serde_json::Value::Array(items) => {
223            for v in items {
224                collect_refs(v, queue, keep);
225            }
226        }
227        _ => {}
228    }
229}
230
231#[derive(Debug, thiserror::Error)]
232pub enum ServerCodegenError {
233    #[error("server selector: {0}")]
234    Parse(#[from] super::SelectorParseError),
235    #[error("server selector: {0}")]
236    Resolve(#[from] super::SelectorResolveError),
237    #[error("internal: {0}")]
238    Internal(String),
239    #[error(
240        "cannot generate exact Axum routes for custom HTTP methods on `{path}` across multiple primary tags ({tags}); Axum cannot merge multiple fallback dispatchers for one path. Put those operations under the same first tag or select only one custom method for this server"
241    )]
242    CrossTagCustomMethods { path: String, tags: String },
243    #[error(
244        "cannot generate Axum server for distinct primary tags `{first_tag}` and `{second_tag}`: both normalize to Rust trait identifier `{identifier}` (and the same router factory name). Rename one tag in the OpenAPI document or a schema overlay, or select the operations in separate generated servers"
245    )]
246    TagIdentifierCollision {
247        first_tag: String,
248        second_tag: String,
249        identifier: String,
250    },
251    #[error("cannot generate Axum route for `{path}`: {reason}")]
252    InvalidRoutePath { path: String, reason: String },
253    #[error(
254        "cannot generate Axum query extraction for `{operation_id}` parameter `{parameter}`: {reason}"
255    )]
256    UnsupportedQueryParameter {
257        operation_id: String,
258        parameter: String,
259        reason: String,
260    },
261    #[error(
262        "cannot generate unambiguous Axum query extraction for `{operation_id}`: wire key `{wire_key}` is claimed by both `{first_parameter}` and `{second_parameter}`"
263    )]
264    AmbiguousQueryParameter {
265        operation_id: String,
266        wire_key: String,
267        first_parameter: String,
268        second_parameter: String,
269    },
270    #[error("request validation: {0}")]
271    Validation(String),
272    #[error(
273        "cannot generate Axum extraction for `{operation_id}` {location} parameter `{parameter}`: only scalar schema serialization is currently supported"
274    )]
275    UnsupportedParameterSerialization {
276        operation_id: String,
277        location: String,
278        parameter: String,
279    },
280    #[error(
281        "cannot generate Axum request body for `{operation_id}` media type `{media_type}`: {reason}"
282    )]
283    UnsupportedRequestBody {
284        operation_id: String,
285        media_type: String,
286        reason: String,
287    },
288    #[error(
289        "cannot generate response for `{operation_id}` status `{status}`: unsupported media content {media_types}"
290    )]
291    UnsupportedResponseContent {
292        operation_id: String,
293        status: String,
294        media_types: String,
295    },
296}
297
298pub struct ServerCodegen<'a> {
299    config: &'a GeneratorConfig,
300    analysis: &'a SchemaAnalysis,
301    server: &'a ServerSection,
302    source_provenance: Option<String>,
303}
304
305impl<'a> ServerCodegen<'a> {
306    fn resolve_multipart_schema<'b>(
307        &'b self,
308        schema: &'b serde_json::Value,
309    ) -> Result<&'b serde_json::Value, ServerCodegenError> {
310        self.resolve_multipart_schema_inner(schema, &mut std::collections::HashSet::new())
311    }
312
313    fn resolve_multipart_schema_inner<'b>(
314        &'b self,
315        schema: &'b serde_json::Value,
316        visited: &mut std::collections::HashSet<String>,
317    ) -> Result<&'b serde_json::Value, ServerCodegenError> {
318        let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) else {
319            return Ok(schema);
320        };
321        let Some(name) = reference.strip_prefix("#/components/schemas/") else {
322            return Ok(schema);
323        };
324        if !visited.insert(name.to_string()) {
325            return Err(ServerCodegenError::Validation(format!(
326                "multipart schema reference cycle includes `{name}`"
327            )));
328        }
329        let resolved = self
330            .analysis
331            .validation_context
332            .component_schemas
333            .get(name)
334            .ok_or_else(|| {
335                ServerCodegenError::Validation(format!(
336                    "multipart schema reference `{reference}` could not be resolved"
337                ))
338            })?;
339        self.resolve_multipart_schema_inner(resolved, visited)
340    }
341
342    fn multipart_field_plans(
343        &self,
344        schema: &serde_json::Value,
345    ) -> Result<Vec<MultipartFieldPlan>, ServerCodegenError> {
346        let schema = self.resolve_multipart_schema(schema)?;
347        let properties = schema
348            .get("properties")
349            .and_then(serde_json::Value::as_object)
350            .ok_or_else(|| {
351                ServerCodegenError::Validation(
352                    "multipart request schema must be a flat object with declared properties"
353                        .into(),
354                )
355            })?;
356        if schema
357            .get("additionalProperties")
358            .is_some_and(|value| value != &serde_json::Value::Bool(false))
359        {
360            return Err(ServerCodegenError::Validation(
361                "multipart request schema cannot use additionalProperties".into(),
362            ));
363        }
364        let required = schema
365            .get("required")
366            .and_then(serde_json::Value::as_array)
367            .into_iter()
368            .flatten()
369            .filter_map(serde_json::Value::as_str)
370            .collect::<std::collections::HashSet<_>>();
371        properties
372            .iter()
373            .map(|(wire_name, property)| {
374                let property = self.resolve_multipart_schema(property)?;
375                let kind = match (
376                    property.get("type").and_then(serde_json::Value::as_str),
377                    property.get("format").and_then(serde_json::Value::as_str),
378                ) {
379                    (Some("string"), Some("binary")) => match self.config.types.binary {
380                        crate::type_mapping::BinaryStrategy::String => MultipartFieldKind::String,
381                        crate::type_mapping::BinaryStrategy::Bytes
382                        | crate::type_mapping::BinaryStrategy::VecU8 => MultipartFieldKind::Binary,
383                    },
384                    (Some("string"), _) => MultipartFieldKind::String,
385                    (Some("integer"), Some("uint32" | "uint64" | "uint"))
386                        if self.config.types.unsigned =>
387                    {
388                        MultipartFieldKind::UnsignedInteger
389                    }
390                    (Some("integer"), _) => MultipartFieldKind::Integer,
391                    (Some("number"), _) => MultipartFieldKind::Number,
392                    (Some("boolean"), _) => MultipartFieldKind::Boolean,
393                    _ => {
394                        return Err(ServerCodegenError::Validation(format!(
395                            "multipart field `{wire_name}` must be binary or a scalar text field"
396                        )));
397                    }
398                };
399                Ok(MultipartFieldPlan {
400                    wire_name: wire_name.clone(),
401                    field_ident: CodeGenerator::to_field_ident(&wire_name.to_snake_case()),
402                    required: required.contains(wire_name.as_str()),
403                    kind,
404                })
405            })
406            .collect()
407    }
408
409    pub fn new(
410        config: &'a GeneratorConfig,
411        analysis: &'a SchemaAnalysis,
412        server: &'a ServerSection,
413    ) -> Self {
414        Self {
415            config,
416            analysis,
417            server,
418            source_provenance: None,
419        }
420    }
421
422    /// Attach a sanitized source label to generated server module headers.
423    pub fn with_source_provenance(mut self, source: Option<&str>) -> Self {
424        self.source_provenance = source.map(str::to_string);
425        self
426    }
427
428    fn provenance_attribute(&self) -> TokenStream {
429        self.source_provenance
430            .as_ref()
431            .map(|source| {
432                let provenance = format!(
433                    " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
434                    env!("CARGO_PKG_VERSION")
435                );
436                quote! { #![doc = #provenance] }
437            })
438            .unwrap_or_default()
439    }
440
441    /// Resolve a model type reference for emitted server modules. Names that
442    /// canonicalize to a different Rust identifier (e.g. `not-found`) cannot
443    /// be parsed from the raw component key and must be qualified explicitly;
444    /// already-canonical names resolve through the module's
445    /// `use super::super::types::*` glob like every other generated reference.
446    fn model_type(&self, ty: &str) -> TokenStream {
447        if self.analysis.schemas.contains_key(ty) {
448            let canonical = rust_type_name(ty);
449            if canonical != ty {
450                let ident = format_ident!("{}", canonical);
451                return quote! { super::super::types::#ident };
452            }
453        }
454        parse_type(ty)
455    }
456
457    /// Resolve selectors and emit `server/{mod,api,errors}.rs`.
458    pub fn generate(&self) -> Result<Vec<GeneratedFile>, ServerCodegenError> {
459        if self.server.operations.is_empty() {
460            return Ok(Vec::new());
461        }
462        if !(1..=100).contains(&self.server.validation.max_errors) {
463            return Err(ServerCodegenError::Validation(
464                "max_errors must be between 1 and 100".to_string(),
465            ));
466        }
467        if !(1..=67_108_864).contains(&self.server.validation.max_body_bytes) {
468            return Err(ServerCodegenError::Validation(
469                "max_body_bytes must be between 1 and 67108864".to_string(),
470            ));
471        }
472
473        let index = OperationIndex::from_analysis(self.analysis);
474        let selectors: Vec<Selector> = self
475            .server
476            .operations
477            .iter()
478            .map(|s| Selector::parse(s))
479            .collect::<Result<_, _>>()?;
480        let resolution = super::resolve(&selectors, &index)?;
481
482        // Look up full OperationInfo for each resolved op (we need
483        // parameters, request body, response schemas — the summary
484        // only has the display surface).
485        let ops: Vec<&OperationInfo> = resolution
486            .operations
487            .iter()
488            .map(|s| {
489                self.analysis
490                    .operations
491                    .get(&s.operation_id)
492                    .ok_or_else(|| {
493                        ServerCodegenError::Internal(format!(
494                            "operation `{}` resolved but missing from analysis",
495                            s.operation_id
496                        ))
497                    })
498            })
499            .collect::<Result<_, _>>()?;
500        validate_tag_identifier_collisions(&ops)?;
501        validate_custom_method_route_groups(&ops)?;
502        validate_normalized_route_collisions(&ops)?;
503        self.validate_query_parameters(&ops)?;
504        self.validate_supported_server_inputs(&ops)?;
505        self.validate_supported_server_outputs(&ops)?;
506        if !self.server.validation.enabled
507            && ops.iter().any(|operation| {
508                operation
509                    .parameters
510                    .iter()
511                    .any(|parameter| matches!(parameter.location.as_str(), "header" | "cookie"))
512                    || matches!(
513                        &operation.request_body,
514                        Some(RequestBodyContent::FormUrlEncoded { .. })
515                    )
516            })
517        {
518            return Err(ServerCodegenError::Validation(
519                "typed header/cookie and form extraction requires server.validation.enabled=true"
520                    .to_string(),
521            ));
522        }
523
524        // Group by primary tag (first tag wins; untagged → "Server").
525        let groups = group_by_tag(&ops);
526        let response_enum_names = self.allocate_response_enum_names(&ops);
527        let has_binary_body = ops.iter().any(|operation| {
528            matches!(
529                operation.request_body,
530                Some(RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. })
531            )
532        });
533        let has_text_body = ops.iter().any(|operation| {
534            matches!(
535                operation.request_body,
536                Some(RequestBodyContent::TextPlain { .. })
537            )
538        });
539        let has_embedded_path_affixes = ops.iter().any(|operation| {
540            operation.parameters.iter().any(|parameter| {
541                parameter.location == "path"
542                    && path_parameter_affixes(&operation.path, &parameter.name).is_some()
543            })
544        });
545
546        let validation_bundle = if self.server.validation.enabled {
547            Some(
548                super::validation::prepare_validation_bundle(
549                    &self.analysis.validation_context,
550                    &ops,
551                )
552                .map_err(|error| ServerCodegenError::Validation(error.to_string()))?,
553            )
554        } else {
555            eprintln!(
556                "⚠️  server.validation.enabled=false: generated handlers will not enforce the OpenAPI request contract"
557            );
558            None
559        };
560
561        let transport_validation = has_binary_body || has_text_body;
562        let validation_module_enabled =
563            validation_bundle.is_some() || transport_validation || has_embedded_path_affixes;
564        let api_rs = self.emit_api(&groups, &response_enum_names);
565        let errors_rs = self.emit_errors(&ops, validation_module_enabled, &response_enum_names);
566        let router_rs = self.emit_router(&groups, validation_bundle.as_ref())?;
567        let mod_rs = self.emit_mod(validation_module_enabled);
568
569        let mut files = vec![
570            GeneratedFile {
571                path: PathBuf::from("server").join("mod.rs"),
572                content: format_or_raw(mod_rs),
573            },
574            GeneratedFile {
575                path: PathBuf::from("server").join("api.rs"),
576                content: format_or_raw(api_rs),
577            },
578            GeneratedFile {
579                path: PathBuf::from("server").join("errors.rs"),
580                content: format_or_raw(errors_rs),
581            },
582            GeneratedFile {
583                path: PathBuf::from("server").join("router.rs"),
584                content: format_or_raw(router_rs),
585            },
586        ];
587        if let Some(bundle) = &validation_bundle {
588            files.push(GeneratedFile {
589                path: PathBuf::from("server").join("validation.rs"),
590                content: format_or_raw(super::validation::emit_validation_module(
591                    bundle,
592                    self.server.validation.max_errors,
593                    has_binary_body,
594                    has_text_body,
595                )),
596            });
597        } else if transport_validation || has_embedded_path_affixes {
598            files.push(GeneratedFile {
599                path: PathBuf::from("server").join("validation.rs"),
600                content: format_or_raw(super::validation::emit_transport_validation_module(
601                    has_binary_body,
602                    has_text_body,
603                )),
604            });
605        }
606        Ok(files)
607    }
608
609    fn query_parameter_type(&self, parameter: &ParameterInfo) -> TokenStream {
610        CodeGenerator::new(self.config.clone()).get_param_owned_rust_type(parameter)
611    }
612
613    fn parameter_schema_is_scalar(&self, schema: &serde_json::Value) -> bool {
614        self.parameter_schema_is_scalar_inner(schema, &mut std::collections::BTreeSet::new())
615    }
616
617    fn parameter_schema_is_string(&self, parameter: &ParameterInfo) -> bool {
618        if parameter.enum_values.is_some() || parameter.rust_type == "String" {
619            return true;
620        }
621        let Some(schema) = parameter.validation_schema.as_ref() else {
622            return false;
623        };
624        self.parameter_schema_is_string_inner(schema, &mut std::collections::BTreeSet::new())
625    }
626
627    fn parameter_schema_is_string_inner(
628        &self,
629        schema: &serde_json::Value,
630        visited: &mut std::collections::BTreeSet<String>,
631    ) -> bool {
632        if let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) {
633            if let Some(name) = reference.strip_prefix("#/components/schemas/") {
634                return self
635                    .analysis
636                    .validation_context
637                    .component_schemas
638                    .get(name)
639                    .is_some_and(|component| {
640                        visited.insert(name.to_string())
641                            && self.parameter_schema_is_string_inner(component, visited)
642                    });
643            }
644        }
645        schema.get("type").and_then(serde_json::Value::as_str) == Some("string")
646    }
647
648    fn parameter_schema_is_scalar_inner(
649        &self,
650        schema: &serde_json::Value,
651        visited: &mut std::collections::BTreeSet<String>,
652    ) -> bool {
653        if let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str)
654            && let Some(name) = reference.strip_prefix("#/components/schemas/")
655            && let Some(component) = self.analysis.validation_context.component_schemas.get(name)
656        {
657            return visited.insert(name.to_string())
658                && self.parameter_schema_is_scalar_inner(component, visited);
659        }
660        !matches!(
661            schema.get("type").and_then(serde_json::Value::as_str),
662            Some("array" | "object")
663        ) && schema.get("oneOf").is_none()
664            && schema.get("anyOf").is_none()
665            && schema.get("allOf").is_none()
666    }
667
668    fn form_field_names(
669        &self,
670        operation: &OperationInfo,
671    ) -> Result<Vec<String>, ServerCodegenError> {
672        let Some(RequestBodyContent::FormUrlEncoded { schema_name, .. }) = &operation.request_body
673        else {
674            return Ok(Vec::new());
675        };
676        let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
677            ServerCodegenError::UnsupportedRequestBody {
678                operation_id: operation.operation_id.clone(),
679                media_type: "application/x-www-form-urlencoded".to_string(),
680                reason: format!("schema `{schema_name}` cannot be resolved"),
681            }
682        })?;
683        let SchemaType::Object {
684            properties,
685            additional_properties,
686            ..
687        } = &schema.schema_type
688        else {
689            return Err(ServerCodegenError::UnsupportedRequestBody {
690                operation_id: operation.operation_id.clone(),
691                media_type: "application/x-www-form-urlencoded".to_string(),
692                reason: "only flat object schemas are supported".to_string(),
693            });
694        };
695        if !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
696            || properties.values().any(|property| {
697                !self.query_property_is_scalar(
698                    &property.schema_type,
699                    &mut std::collections::HashSet::new(),
700                )
701            })
702        {
703            return Err(ServerCodegenError::UnsupportedRequestBody {
704                operation_id: operation.operation_id.clone(),
705                media_type: "application/x-www-form-urlencoded".to_string(),
706                reason: "only flat scalar fields with additionalProperties forbidden are supported"
707                    .to_string(),
708            });
709        }
710        Ok(properties.keys().cloned().collect())
711    }
712
713    fn validate_supported_server_inputs(
714        &self,
715        operations: &[&OperationInfo],
716    ) -> Result<(), ServerCodegenError> {
717        for operation in operations {
718            for parameter in &operation.parameters {
719                if !matches!(
720                    parameter.location.as_str(),
721                    "path" | "query" | "header" | "cookie"
722                ) {
723                    return Err(ServerCodegenError::UnsupportedParameterSerialization {
724                        operation_id: operation.operation_id.clone(),
725                        location: parameter.location.clone(),
726                        parameter: parameter.name.clone(),
727                    });
728                }
729                if matches!(parameter.location.as_str(), "path" | "header" | "cookie")
730                    && !matches!(
731                        parameter.query_serialization,
732                        Some(QuerySerialization::SimpleHeaderArray { .. })
733                    )
734                    && parameter
735                        .validation_schema
736                        .as_ref()
737                        .is_none_or(|schema| !self.parameter_schema_is_scalar(schema))
738                {
739                    return Err(ServerCodegenError::UnsupportedParameterSerialization {
740                        operation_id: operation.operation_id.clone(),
741                        location: parameter.location.clone(),
742                        parameter: parameter.name.clone(),
743                    });
744                }
745            }
746            match &operation.request_body {
747                Some(RequestBodyContent::FormUrlEncoded { .. }) => {
748                    self.form_field_names(operation)?;
749                }
750                Some(RequestBodyContent::Multipart {
751                    validation_schema, ..
752                }) => {
753                    self.multipart_field_plans(validation_schema)?;
754                }
755                Some(RequestBodyContent::SchemaLess { media_type }) => {
756                    return Err(ServerCodegenError::UnsupportedRequestBody {
757                        operation_id: operation.operation_id.clone(),
758                        media_type: media_type.clone(),
759                        reason: "request content has no schema to validate".to_string(),
760                    });
761                }
762                Some(RequestBodyContent::Unsupported { media_types }) => {
763                    return Err(ServerCodegenError::UnsupportedRequestBody {
764                        operation_id: operation.operation_id.clone(),
765                        media_type: media_types.join(", "),
766                        reason: "the selected request body uses unsupported media content"
767                            .to_string(),
768                    });
769                }
770                _ => {}
771            }
772        }
773        Ok(())
774    }
775
776    fn validate_supported_server_outputs(
777        &self,
778        operations: &[&OperationInfo],
779    ) -> Result<(), ServerCodegenError> {
780        for operation in operations {
781            if let Some(responses) = self
782                .analysis
783                .operation_responses
784                .get(&operation.operation_id)
785            {
786                for (status, response) in responses {
787                    // A Response Object may advertise multiple representations.
788                    // Generation is viable whenever at least one buffered or SSE
789                    // representation can be emitted; unsupported alternatives
790                    // do not invalidate that supported path.
791                    if response.has_content
792                        && response.body.is_none()
793                        && response.schema_name.is_none()
794                        && !response.supports_streaming
795                    {
796                        return Err(ServerCodegenError::UnsupportedResponseContent {
797                            operation_id: operation.operation_id.clone(),
798                            status: status.clone(),
799                            media_types: if response.unsupported_media_types.is_empty() {
800                                "(schema-less content)".to_string()
801                            } else {
802                                response.unsupported_media_types.join(", ")
803                            },
804                        });
805                    }
806                }
807            }
808        }
809        Ok(())
810    }
811
812    fn parameter_ident(&self, parameter: &ParameterInfo) -> syn::Ident {
813        let generator = CodeGenerator::new(self.config.clone());
814        CodeGenerator::to_field_ident(&generator.param_ident_str(parameter))
815    }
816
817    /// Allocate public response-enum identifiers without colliding with the
818    /// retained models that the generated server modules glob-import.
819    fn allocate_response_enum_names(&self, ops: &[&OperationInfo]) -> BTreeMap<String, syn::Ident> {
820        let generator = CodeGenerator::new(self.config.clone());
821        let mut used_names: std::collections::BTreeSet<String> = self
822            .analysis
823            .schemas
824            .keys()
825            .map(|name| generator.to_rust_type_name(name))
826            .collect();
827
828        // Parameter enums are emitted directly into api.rs rather than into
829        // analysis.schemas, but share that module's type namespace with the
830        // imported response enums.
831        used_names.extend(
832            ops.iter()
833                .flat_map(|operation| operation.parameters.iter())
834                .filter(|parameter| parameter.enum_values.is_some())
835                .map(|parameter| parameter.rust_type.clone()),
836        );
837
838        // Selector ordering is user-controlled. Allocate in operation-id order
839        // so reversing selectors cannot change generated public identifiers.
840        let mut sorted_ops = ops.to_vec();
841        sorted_ops.sort_by(|left, right| left.operation_id.cmp(&right.operation_id));
842
843        let mut names = BTreeMap::new();
844        for operation in sorted_ops {
845            let operation_name = operation.operation_id.to_pascal_case();
846            let preferred = format!("{operation_name}Response");
847            let chosen = if used_names.insert(preferred.clone()) {
848                preferred
849            } else {
850                let fallback = format!("{operation_name}ServerResponse");
851                let mut candidate = fallback.clone();
852                let mut suffix = 2;
853                while !used_names.insert(candidate.clone()) {
854                    candidate = format!("{fallback}{suffix}");
855                    suffix += 1;
856                }
857                candidate
858            };
859            names.insert(operation.operation_id.clone(), format_ident!("{chosen}"));
860        }
861        names
862    }
863
864    fn validation_target(
865        &self,
866        bundle: Option<&super::validation::ValidationBundle>,
867        operation: &OperationInfo,
868        location: &str,
869        parameter_name: Option<&str>,
870    ) -> Result<Option<TokenStream>, ServerCodegenError> {
871        let Some(bundle) = bundle else {
872            return Ok(None);
873        };
874        let target = bundle
875            .target_for(&operation.operation_id, location, parameter_name)
876            .ok_or_else(|| {
877                ServerCodegenError::Validation(format!(
878                    "missing generated validator target for operation `{}` {location} `{}`",
879                    operation.operation_id,
880                    parameter_name.unwrap_or("body")
881                ))
882            })?;
883        let ident = format_ident!("{}", target.constant);
884        Ok(Some(quote! { super::validation::#ident }))
885    }
886
887    fn resolve_query_schema(&self, schema_name: &str) -> Option<&crate::analysis::AnalyzedSchema> {
888        let mut current = schema_name;
889        let mut visited = std::collections::HashSet::new();
890        loop {
891            if !visited.insert(current) {
892                return None;
893            }
894            let schema = self.analysis.schemas.get(current)?;
895            if let SchemaType::Reference { target } = &schema.schema_type {
896                current = target;
897            } else {
898                return Some(schema);
899            }
900        }
901    }
902
903    fn query_object_properties(
904        &self,
905        parameter: &ParameterInfo,
906    ) -> Option<&BTreeMap<String, crate::analysis::PropertyInfo>> {
907        let schema = self.resolve_query_schema(parameter.schema_ref.as_deref()?)?;
908        match &schema.schema_type {
909            SchemaType::Object { properties, .. } => Some(properties),
910            _ => None,
911        }
912    }
913
914    fn query_object_required_properties(&self, parameter: &ParameterInfo) -> Vec<String> {
915        let Some(schema) = parameter
916            .schema_ref
917            .as_deref()
918            .and_then(|name| self.resolve_query_schema(name))
919        else {
920            return Vec::new();
921        };
922        let mut names = match &schema.schema_type {
923            SchemaType::Object { required, .. } => required.iter().cloned().collect(),
924            _ => Vec::new(),
925        };
926        names.sort();
927        names
928    }
929
930    fn form_required_field_names(
931        &self,
932        operation: &OperationInfo,
933    ) -> Result<Vec<String>, ServerCodegenError> {
934        let Some(RequestBodyContent::FormUrlEncoded { schema_name, .. }) = &operation.request_body
935        else {
936            return Ok(Vec::new());
937        };
938        let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
939            ServerCodegenError::UnsupportedRequestBody {
940                operation_id: operation.operation_id.clone(),
941                media_type: "application/x-www-form-urlencoded".to_string(),
942                reason: format!("schema `{schema_name}` cannot be resolved"),
943            }
944        })?;
945        let mut names = match &schema.schema_type {
946            SchemaType::Object { required, .. } => required.iter().cloned().collect(),
947            _ => Vec::new(),
948        };
949        names.sort();
950        Ok(names)
951    }
952
953    fn query_property_is_scalar(
954        &self,
955        schema_type: &SchemaType,
956        visited: &mut std::collections::HashSet<String>,
957    ) -> bool {
958        match schema_type {
959            SchemaType::Primitive { .. }
960            | SchemaType::StringEnum { .. }
961            | SchemaType::ExtensibleEnum { .. } => true,
962            SchemaType::Reference { target } if visited.insert(target.clone()) => {
963                self.analysis.schemas.get(target).is_some_and(|schema| {
964                    self.query_property_is_scalar(&schema.schema_type, visited)
965                })
966            }
967            _ => false,
968        }
969    }
970
971    fn validate_query_object(
972        &self,
973        operation: &OperationInfo,
974        parameter: &ParameterInfo,
975    ) -> Result<Vec<String>, ServerCodegenError> {
976        let error = |reason: String| ServerCodegenError::UnsupportedQueryParameter {
977            operation_id: operation.operation_id.clone(),
978            parameter: parameter.name.clone(),
979            reason,
980        };
981        let schema_name = parameter.schema_ref.as_deref().ok_or_else(|| {
982            error("styled object parameter has no analyzed schema type".to_string())
983        })?;
984        let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
985            error(format!(
986                "query object schema `{schema_name}` could not be resolved"
987            ))
988        })?;
989        let (properties, additional_properties) = match &schema.schema_type {
990            SchemaType::Object {
991                properties,
992                additional_properties,
993                ..
994            } => (properties, additional_properties),
995            _ => {
996                return Err(error(format!(
997                    "query schema `{schema_name}` does not resolve to a flat object"
998                )));
999            }
1000        };
1001        if !matches!(additional_properties, ObjectAdditionalProperties::Forbidden) {
1002            return Err(error(
1003                "styled object parameters with additionalProperties have an ambiguous wire namespace"
1004                    .to_string(),
1005            ));
1006        }
1007        for (property_name, property) in properties {
1008            if !self.query_property_is_scalar(
1009                &property.schema_type,
1010                &mut std::collections::HashSet::new(),
1011            ) {
1012                return Err(error(format!(
1013                    "property `{property_name}` is not scalar; nested arrays/objects are undefined for the generated query wire format"
1014                )));
1015            }
1016        }
1017        Ok(properties.keys().cloned().collect())
1018    }
1019
1020    fn validate_query_parameters(
1021        &self,
1022        operations: &[&OperationInfo],
1023    ) -> Result<(), ServerCodegenError> {
1024        for operation in operations {
1025            let mut claimed_keys: BTreeMap<String, String> = BTreeMap::new();
1026            for parameter in operation
1027                .parameters
1028                .iter()
1029                .filter(|parameter| parameter.location == "query")
1030            {
1031                let mut keys = match &parameter.query_serialization {
1032                    Some(QuerySerialization::Unsupported { reason }) => {
1033                        return Err(ServerCodegenError::UnsupportedQueryParameter {
1034                            operation_id: operation.operation_id.clone(),
1035                            parameter: parameter.name.clone(),
1036                            reason: reason.clone(),
1037                        });
1038                    }
1039                    Some(
1040                        QuerySerialization::FormExplodedObject
1041                        | QuerySerialization::FormObject
1042                        | QuerySerialization::DeepObject,
1043                    ) => {
1044                        let property_keys = self.validate_query_object(operation, parameter)?;
1045                        if matches!(
1046                            parameter.query_serialization,
1047                            Some(QuerySerialization::FormExplodedObject)
1048                        ) {
1049                            property_keys
1050                        } else if matches!(
1051                            parameter.query_serialization,
1052                            Some(QuerySerialization::DeepObject)
1053                        ) {
1054                            property_keys
1055                                .into_iter()
1056                                .map(|property| format!("{}[{property}]", parameter.name))
1057                                .collect()
1058                        } else {
1059                            vec![parameter.name.clone()]
1060                        }
1061                    }
1062                    Some(QuerySerialization::FormExplodedNestedObject { .. }) => {
1063                        vec![parameter.name.clone()]
1064                    }
1065                    Some(
1066                        QuerySerialization::FormExplodedArray { .. }
1067                        | QuerySerialization::FormArray { .. }
1068                        | QuerySerialization::SimpleHeaderArray { .. },
1069                    )
1070                    | None => vec![parameter.name.clone()],
1071                };
1072                if matches!(
1073                    &parameter.query_serialization,
1074                    Some(
1075                        QuerySerialization::FormExplodedObject
1076                            | QuerySerialization::FormExplodedNestedObject { .. }
1077                            | QuerySerialization::FormObject
1078                            | QuerySerialization::DeepObject
1079                            | QuerySerialization::FormExplodedArray { .. }
1080                            | QuerySerialization::FormArray { .. }
1081                    )
1082                ) {
1083                    keys.push(format!("{}[]", parameter.name));
1084                }
1085                for key in keys {
1086                    if let Some(first_parameter) =
1087                        claimed_keys.insert(key.clone(), parameter.name.clone())
1088                    {
1089                        return Err(ServerCodegenError::AmbiguousQueryParameter {
1090                            operation_id: operation.operation_id.clone(),
1091                            wire_key: key,
1092                            first_parameter,
1093                            second_parameter: parameter.name.clone(),
1094                        });
1095                    }
1096                }
1097            }
1098        }
1099        Ok(())
1100    }
1101
1102    fn emit_mod(&self, validation_enabled: bool) -> TokenStream {
1103        let provenance_attribute = self.provenance_attribute();
1104        let validation_module = validation_enabled.then(|| quote! { pub(crate) mod validation; });
1105        quote! {
1106            //! Server scaffolding emitted by openapi-to-rust.
1107            //!
1108            //! Implement the per-tag trait(s) in `api` on your own struct,
1109            //! then build an `axum::Router` via `router::router(impl)`.
1110
1111            #provenance_attribute
1112
1113            pub mod api;
1114            pub mod errors;
1115            pub mod router;
1116            #validation_module
1117
1118            pub use api::*;
1119            pub use errors::*;
1120            pub use router::*;
1121        }
1122    }
1123
1124    fn emit_router(
1125        &self,
1126        groups: &BTreeMap<String, Vec<&OperationInfo>>,
1127        validation_bundle: Option<&super::validation::ValidationBundle>,
1128    ) -> Result<TokenStream, ServerCodegenError> {
1129        let provenance_attribute = self.provenance_attribute();
1130        let factories: Vec<TokenStream> = groups
1131            .iter()
1132            .map(|(tag, ops)| self.emit_router_for_trait(tag, ops, validation_bundle))
1133            .collect::<Result<_, _>>()?;
1134
1135        // Per-op Query structs — one per op that has any query params.
1136        let query_structs: Vec<TokenStream> = groups
1137            .values()
1138            .flatten()
1139            .filter_map(|op| self.emit_query_struct(op))
1140            .collect();
1141        let has_query_parameters = groups.values().flatten().any(|operation| {
1142            operation
1143                .parameters
1144                .iter()
1145                .any(|parameter| parameter.location == "query")
1146        });
1147        let query_helpers = has_query_parameters.then(|| {
1148            quote! {
1149                fn __query_pairs(raw: ::std::option::Option<&str>) -> ::std::vec::Vec<(String, String)> {
1150                    raw.map(|query| {
1151                        ::url::form_urlencoded::parse(query.as_bytes())
1152                            .into_owned()
1153                            .collect()
1154                    })
1155                    .unwrap_or_default()
1156                }
1157
1158                fn __validate_urlencoded(raw: &str) -> ::std::result::Result<(), String> {
1159                    let bytes = raw.as_bytes();
1160                    let mut index = 0;
1161                    while index < bytes.len() {
1162                        if bytes[index] == b'%' {
1163                            if index + 2 >= bytes.len()
1164                                || !bytes[index + 1].is_ascii_hexdigit()
1165                                || !bytes[index + 2].is_ascii_hexdigit()
1166                            {
1167                                return Err("malformed percent encoding".to_string());
1168                            }
1169                            index += 3;
1170                        } else {
1171                            index += 1;
1172                        }
1173                    }
1174                    Ok(())
1175                }
1176
1177                fn __query_one(
1178                    pairs: &[(String, String)],
1179                    key: &str,
1180                ) -> ::std::result::Result<::std::option::Option<String>, String> {
1181                    let mut values = pairs
1182                        .iter()
1183                        .filter(|(candidate, _)| candidate == key)
1184                        .map(|(_, value)| value.clone());
1185                    let value = values.next();
1186                    if values.next().is_some() {
1187                        return Err(format!("query parameter `{key}` appeared more than once"));
1188                    }
1189                    Ok(value)
1190                }
1191
1192                fn __decode_query_scalar<T>(
1193                    value: &str,
1194                    label: &str,
1195                ) -> ::std::result::Result<T, String>
1196                where
1197                    T: ::serde::de::DeserializeOwned,
1198                {
1199                    ::serde_json::from_value(::serde_json::Value::String(value.to_string()))
1200                        .or_else(|_| ::serde_json::from_str(value))
1201                        .map_err(|error| format!("invalid query value for `{label}`: {error}"))
1202                }
1203
1204                fn __decode_query_object<T>(
1205                    fields: &[(String, String)],
1206                    label: &str,
1207                ) -> ::std::result::Result<T, String>
1208                where
1209                    T: ::serde::de::DeserializeOwned,
1210                {
1211                    let mut serializer =
1212                        ::url::form_urlencoded::Serializer::new(String::new());
1213                    for (key, value) in fields {
1214                        serializer.append_pair(key, value);
1215                    }
1216                    ::serde_urlencoded::from_str(&serializer.finish())
1217                        .map_err(|error| format!("invalid query object `{label}`: {error}"))
1218                }
1219
1220                fn __query_empty_marker(
1221                    pairs: &[(String, String)],
1222                    key: &str,
1223                ) -> ::std::result::Result<bool, String> {
1224                    let marker = format!("{key}[]");
1225                    match __query_one(pairs, &marker)? {
1226                        Some(value) if value.is_empty() => Ok(true),
1227                        Some(_) => Err(format!(
1228                            "zero-cardinality marker `{marker}` must have an empty value"
1229                        )),
1230                        None => Ok(false),
1231                    }
1232                }
1233            }
1234        });
1235
1236        // When the picked operations span multiple tags, emit a
1237        // top-level `build_router(impl1, impl2, ...)` that takes one
1238        // generic per trait and `.merge()`s the per-tag factories.
1239        // For a single-tag selection this is unnecessary noise — the
1240        // user calls the per-tag factory directly.
1241        let combined = if groups.len() > 1 {
1242            Some(self.emit_combined_router(groups))
1243        } else {
1244            None
1245        };
1246
1247        Ok(quote! {
1248            //! Router factories — one per trait. Each takes any
1249            //! `T: <TraitName> + Clone + Send + Sync + 'static` and
1250            //! returns an `axum::Router` with state pre-attached.
1251
1252            #provenance_attribute
1253
1254            use super::api::*;
1255            use super::errors::*;
1256            // Pull schemas directly from the types module (always a
1257            // sibling of mod.rs). Doesn't rely on the parent module
1258            // re-exporting types::*, so users can mount the generated
1259            // tree at any path without rewriting these imports.
1260            #[allow(unused_imports)]
1261            use super::super::types::*;
1262
1263            #query_helpers
1264
1265            #(#query_structs)*
1266
1267            #(#factories)*
1268
1269            #combined
1270        })
1271    }
1272
1273    fn emit_combined_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
1274        // Stable ordering: BTreeMap iteration is already alphabetical
1275        // by tag, which gives us deterministic generic ordering across
1276        // generator runs.
1277        let entries: Vec<(syn::Ident, syn::Ident, syn::Ident)> = groups
1278            .keys()
1279            .enumerate()
1280            .map(|(i, tag)| {
1281                let trait_ident = trait_ident_for_tag(tag);
1282                let factory = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
1283                let generic = format_ident!("T{}", i + 1);
1284                (trait_ident, factory, generic)
1285            })
1286            .collect();
1287
1288        let generics: Vec<&syn::Ident> = entries.iter().map(|(_, _, g)| g).collect();
1289        let args: Vec<TokenStream> = entries
1290            .iter()
1291            .map(|(trait_ident, _, g)| {
1292                let arg_ident = format_ident!("{}", trait_ident.to_string().to_snake_case());
1293                quote! { #arg_ident: #g }
1294            })
1295            .collect();
1296        let bounds: Vec<TokenStream> = entries
1297            .iter()
1298            .map(|(trait_ident, _, g)| {
1299                quote! { #g: #trait_ident + Clone + Send + Sync + 'static }
1300            })
1301            .collect();
1302
1303        // Fold the factories: `factory1(arg1).merge(factory2(arg2)).merge(...)`.
1304        let first = &entries[0];
1305        let first_arg = format_ident!("{}", first.0.to_string().to_snake_case());
1306        let first_factory = &first.1;
1307        let rest = entries
1308            .iter()
1309            .skip(1)
1310            .map(|(trait_ident, factory, _)| {
1311                let arg = format_ident!("{}", trait_ident.to_string().to_snake_case());
1312                quote! { .merge(#factory(#arg)) }
1313            })
1314            .collect::<Vec<_>>();
1315
1316        let trait_names: Vec<String> = entries.iter().map(|(t, _, _)| t.to_string()).collect();
1317        let doc = format!(
1318            " Combined router spanning {} traits: {}.",
1319            entries.len(),
1320            trait_names.join(", "),
1321        );
1322
1323        quote! {
1324            #[doc = #doc]
1325            pub fn build_router<#(#generics),*>(
1326                #(#args),*
1327            ) -> ::axum::Router
1328            where
1329                #(#bounds),*
1330            {
1331                #first_factory(#first_arg) #(#rest)*
1332            }
1333        }
1334    }
1335
1336    fn emit_router_for_trait(
1337        &self,
1338        tag: &str,
1339        ops: &[&OperationInfo],
1340        validation_bundle: Option<&super::validation::ValidationBundle>,
1341    ) -> Result<TokenStream, ServerCodegenError> {
1342        let trait_ident = trait_ident_for_tag(tag);
1343        let fn_ident = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
1344
1345        let mut routes: Vec<TokenStream> = Vec::new();
1346        let mut custom_by_path: BTreeMap<String, Vec<(String, syn::Ident)>> = BTreeMap::new();
1347        for op in ops {
1348            let handler = format_ident!("{}_handler", op.operation_id.to_snake_case());
1349            let path = openapi_to_axum_path(&op.path)?;
1350            if let Some(method_call) = axum_method_call(&op.method) {
1351                routes.push(quote! { .route(#path, ::axum::routing::#method_call(#handler::<T>)) });
1352            } else {
1353                custom_by_path
1354                    .entry(path)
1355                    .or_default()
1356                    .push((op.method.to_ascii_uppercase(), handler));
1357            }
1358        }
1359        let mut custom_dispatchers = Vec::new();
1360        for (path, methods) in custom_by_path {
1361            let first_handler = &methods[0].1;
1362            let dispatcher = format_ident!("{}_custom_method_dispatch", first_handler);
1363            let (route, dispatcher_fn) =
1364                axum_custom_route(&path, &dispatcher, &methods, &trait_ident);
1365            routes.push(route);
1366            custom_dispatchers.push(dispatcher_fn);
1367        }
1368
1369        let handlers: Vec<TokenStream> = ops
1370            .iter()
1371            .map(|op| self.emit_axum_handler(&trait_ident, op, validation_bundle))
1372            .collect::<Result<_, _>>()?;
1373
1374        let doc = format!(" Build an axum::Router for the `{trait_ident}` trait.");
1375        let max_body_bytes = self.server.validation.max_body_bytes;
1376
1377        Ok(quote! {
1378            #[doc = #doc]
1379            pub fn #fn_ident<T>(api: T) -> ::axum::Router
1380            where
1381                T: #trait_ident + Clone + Send + Sync + 'static,
1382            {
1383                ::axum::Router::new()
1384                    #(#routes)*
1385                    .layer(::axum::extract::DefaultBodyLimit::max(#max_body_bytes))
1386                    .with_state(api)
1387            }
1388
1389            #(#custom_dispatchers)*
1390
1391            #(#handlers)*
1392        })
1393    }
1394
1395    fn emit_axum_handler(
1396        &self,
1397        trait_ident: &syn::Ident,
1398        op: &OperationInfo,
1399        validation_bundle: Option<&super::validation::ValidationBundle>,
1400    ) -> Result<TokenStream, ServerCodegenError> {
1401        let handler_ident = format_ident!("{}_handler", op.operation_id.to_snake_case());
1402        let trait_method = format_ident!("{}", op.operation_id.to_snake_case());
1403
1404        // Build extractor list + call argument list.
1405        let mut extractors: Vec<TokenStream> =
1406            vec![quote! { ::axum::extract::State(api): ::axum::extract::State<T> }];
1407        let mut call_args: Vec<TokenStream> = Vec::new();
1408
1409        // Path parameters. With validation enabled, extract by wire name so
1410        // declaration order cannot drift from the route template and all
1411        // malformed values use the public rejection profile.
1412        let path_params: Vec<&_> = op
1413            .parameters
1414            .iter()
1415            .filter(|p| p.location == "path")
1416            .collect();
1417        let path_has_affixes = path_params
1418            .iter()
1419            .any(|parameter| path_parameter_affixes(&op.path, &parameter.name).is_some());
1420        let mut path_decode = TokenStream::new();
1421        if !path_params.is_empty() && (validation_bundle.is_some() || path_has_affixes) {
1422            extractors.push(quote! {
1423                __path_result: ::std::result::Result<
1424                    ::axum::extract::Path<::std::collections::HashMap<String, String>>,
1425                    ::axum::extract::rejection::PathRejection,
1426                >
1427            });
1428            let mut decoders = Vec::new();
1429            for parameter in &path_params {
1430                let ident = self.parameter_ident(parameter);
1431                let ty = self.query_parameter_type(parameter);
1432                let wire = parameter.name.as_str();
1433                let location = parameter_location("path", wire);
1434                let target = self.validation_target(validation_bundle, op, "path", Some(wire))?;
1435                let string_wire = self.parameter_schema_is_string(parameter);
1436                let strip_affixes = match path_parameter_affixes(&op.path, wire) {
1437                    Some((prefix, suffix)) => quote! {
1438                        let raw = match raw
1439                            .strip_prefix(#prefix)
1440                            .and_then(|value| value.strip_suffix(#suffix))
1441                        {
1442                            Some(value) => value.to_string(),
1443                            None => return ::axum::response::IntoResponse::into_response(
1444                                ::axum::http::StatusCode::NOT_FOUND
1445                            ),
1446                        };
1447                    },
1448                    None => TokenStream::new(),
1449                };
1450                let decode = if let Some(target) = target {
1451                    quote! {
1452                        match super::validation::decode_parameter(
1453                            &raw, #target, #location, #string_wire,
1454                        ) {
1455                            Ok(value) => value,
1456                            Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1457                        }
1458                    }
1459                } else {
1460                    quote! {
1461                        match ::serde_json::from_value(::serde_json::Value::String(raw.clone()))
1462                            .or_else(|_| ::serde_json::from_str(&raw))
1463                        {
1464                            Ok(value) => value,
1465                            Err(_) => return ::axum::response::IntoResponse::into_response(
1466                                ::axum::http::StatusCode::BAD_REQUEST
1467                            ),
1468                        }
1469                    }
1470                };
1471                decoders.push(quote! {
1472                    let #ident: #ty = match __path_values.remove(#wire) {
1473                        Some(raw) => {
1474                            #strip_affixes
1475                            #decode
1476                        },
1477                        None => return ::axum::response::IntoResponse::into_response(
1478                            ::axum::http::StatusCode::INTERNAL_SERVER_ERROR
1479                        ),
1480                    };
1481                });
1482                call_args.push(quote! { #ident });
1483            }
1484            path_decode = quote! {
1485                let ::axum::extract::Path(mut __path_values) = match __path_result {
1486                    Ok(path) => path,
1487                    Err(_) => return ::axum::response::IntoResponse::into_response(
1488                        ::axum::http::StatusCode::BAD_REQUEST
1489                    ),
1490                };
1491                #(#decoders)*
1492            };
1493        } else if !path_params.is_empty() {
1494            let idents: Vec<syn::Ident> = path_params
1495                .iter()
1496                .map(|p| self.parameter_ident(p))
1497                .collect();
1498            let types: Vec<TokenStream> = path_params
1499                .iter()
1500                .map(|p| self.query_parameter_type(p))
1501                .collect();
1502            if path_params.len() == 1 {
1503                let i = &idents[0];
1504                let t = &types[0];
1505                extractors.push(quote! { ::axum::extract::Path(#i): ::axum::extract::Path<#t> });
1506            } else {
1507                extractors.push(quote! { ::axum::extract::Path((#(#idents),*)): ::axum::extract::Path<(#(#types),*)> });
1508            }
1509            for i in &idents {
1510                call_args.push(quote! { #i });
1511            }
1512        }
1513
1514        // Query parameters — extract via a per-op `<Op>Query` struct
1515        // (emitted in the same router.rs above). Required params are
1516        // unwrapped here (short-circuit 400 if missing) so the trait
1517        // method sees a `T` rather than `Option<T>`.
1518        let query_params: Vec<&_> = op
1519            .parameters
1520            .iter()
1521            .filter(|p| p.location == "query")
1522            .collect();
1523        let mut required_query_checks: Vec<TokenStream> = Vec::new();
1524        let mut query_validation_checks: Vec<TokenStream> = Vec::new();
1525        let mut raw_query_validation_checks: Vec<TokenStream> = Vec::new();
1526        let mut query_decode = TokenStream::new();
1527        if !query_params.is_empty() {
1528            let query_ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
1529            let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
1530            extractors.push(quote! {
1531                ::axum::extract::RawQuery(__raw_query): ::axum::extract::RawQuery
1532            });
1533            query_decode = if validation_bundle.is_some() {
1534                quote! {
1535                    let __q: #query_ident = match #decode_ident(__raw_query.as_deref()) {
1536                        Ok(query) => query,
1537                        Err(_) => return ::axum::response::IntoResponse::into_response(
1538                            super::validation::malformed_parameter("/query")
1539                        ),
1540                    };
1541                }
1542            } else {
1543                quote! {
1544                    let __q: #query_ident = match #decode_ident(__raw_query.as_deref()) {
1545                        Ok(query) => query,
1546                        Err(message) => return ::axum::response::IntoResponse::into_response(
1547                            (
1548                                ::axum::http::StatusCode::BAD_REQUEST,
1549                                ::axum::Json(::serde_json::json!({ "error": message })),
1550                            )
1551                        ),
1552                    };
1553                }
1554            };
1555            for p in &query_params {
1556                let f = self.parameter_ident(p);
1557                let wire = p.name.as_str();
1558                let location = parameter_location("query", wire);
1559                let target = self.validation_target(validation_bundle, op, "query", Some(wire))?;
1560                if p.query_serialization.is_none() && self.parameter_schema_is_string(p) {
1561                    if let Some(target) = target.as_ref() {
1562                        raw_query_validation_checks.push(quote! {
1563                            if let Ok(Some(raw)) = __query_one(&__raw_query_pairs, #wire) {
1564                                if let Err(rejection) = super::validation::validate_string_parameter(
1565                                    #target, #location, &raw,
1566                                ) {
1567                                    return ::axum::response::IntoResponse::into_response(rejection);
1568                                }
1569                            }
1570                        });
1571                    }
1572                }
1573                if p.required {
1574                    required_query_checks.push(if validation_bundle.is_some() {
1575                        quote! {
1576                            let #f = match __q.#f {
1577                                Some(v) => v,
1578                                None => return ::axum::response::IntoResponse::into_response(
1579                                    super::validation::missing_parameter(#location)
1580                                ),
1581                            };
1582                        }
1583                    } else {
1584                        let missing_msg = format!("missing required query parameter `{wire}`");
1585                        quote! {
1586                            let #f = match __q.#f {
1587                                Some(v) => v,
1588                                None => return ::axum::response::IntoResponse::into_response(
1589                                    (
1590                                        ::axum::http::StatusCode::BAD_REQUEST,
1591                                        ::axum::Json(::serde_json::json!({
1592                                            "error": #missing_msg
1593                                        })),
1594                                    )
1595                                ),
1596                            };
1597                        }
1598                    });
1599                    if let Some(target) = target {
1600                        query_validation_checks.push(quote! {
1601                            if let Err(rejection) = super::validation::validate_parameter(
1602                                #target, #location, &#f,
1603                            ) {
1604                                return ::axum::response::IntoResponse::into_response(rejection);
1605                            }
1606                        });
1607                    }
1608                    call_args.push(quote! { #f });
1609                } else {
1610                    if let Some(target) = target {
1611                        query_validation_checks.push(quote! {
1612                            if let Some(value) = &__q.#f {
1613                                if let Err(rejection) = super::validation::validate_parameter(
1614                                    #target, #location, value,
1615                                ) {
1616                                    return ::axum::response::IntoResponse::into_response(rejection);
1617                                }
1618                            }
1619                        });
1620                    }
1621                    call_args.push(quote! { __q.#f });
1622                }
1623            }
1624        }
1625
1626        // Scalar header and cookie parameters are decoded to their generated
1627        // Rust types before schema validation. Raw transport/parser errors are
1628        // deliberately discarded at the public boundary.
1629        let header_params: Vec<&_> = op
1630            .parameters
1631            .iter()
1632            .filter(|p| p.location == "header")
1633            .collect();
1634        let cookie_params: Vec<&_> = op
1635            .parameters
1636            .iter()
1637            .filter(|p| p.location == "cookie")
1638            .collect();
1639        let mut parameter_decode_checks: Vec<TokenStream> = Vec::new();
1640        if !header_params.is_empty() || !cookie_params.is_empty() {
1641            extractors.push(quote! { __headers: ::axum::http::HeaderMap });
1642        }
1643        if !header_params.is_empty() {
1644            for p in &header_params {
1645                let wire = p.name.as_str();
1646                let ident = self.parameter_ident(p);
1647                let ty = self.query_parameter_type(p);
1648                let location = parameter_location("header", wire);
1649                let target = self.validation_target(validation_bundle, op, "header", Some(wire))?;
1650                let string_wire = self.parameter_schema_is_string(p);
1651                if matches!(
1652                    p.query_serialization,
1653                    Some(QuerySerialization::SimpleHeaderArray { .. })
1654                ) {
1655                    let decode_array = quote! {
1656                        raw.split(',')
1657                            .map(|item| __decode_query_scalar(item, #wire))
1658                            .collect::<::std::result::Result<#ty, _>>()
1659                    };
1660                    if p.required {
1661                        parameter_decode_checks.push(quote! {
1662                            let mut __values = __headers.get_all(#wire).iter();
1663                            let #ident: #ty = match (__values.next(), __values.next()) {
1664                                (Some(value), None) => match value.to_str() {
1665                                    Ok(raw) => match #decode_array {
1666                                        Ok(value) => value,
1667                                        Err(_) => return ::axum::response::IntoResponse::into_response(
1668                                            super::validation::malformed_parameter(#location)
1669                                        ),
1670                                    },
1671                                    Err(_) => return ::axum::response::IntoResponse::into_response(
1672                                        super::validation::malformed_parameter(#location)
1673                                    ),
1674                                },
1675                                (None, _) => return ::axum::response::IntoResponse::into_response(
1676                                    super::validation::missing_parameter(#location)
1677                                ),
1678                                _ => return ::axum::response::IntoResponse::into_response(
1679                                    super::validation::malformed_parameter(#location)
1680                                ),
1681                            };
1682                        });
1683                        if let Some(target) = target {
1684                            parameter_decode_checks.push(quote! {
1685                                if let Err(rejection) = super::validation::validate_parameter(
1686                                    #target, #location, &#ident,
1687                                ) {
1688                                    return ::axum::response::IntoResponse::into_response(rejection);
1689                                }
1690                            });
1691                        }
1692                        call_args.push(quote! { #ident });
1693                    } else {
1694                        parameter_decode_checks.push(quote! {
1695                            let mut __values = __headers.get_all(#wire).iter();
1696                            let #ident: ::std::option::Option<#ty> = match (__values.next(), __values.next()) {
1697                                (Some(value), None) => match value.to_str() {
1698                                    Ok(raw) => match #decode_array {
1699                                        Ok(value) => Some(value),
1700                                        Err(_) => return ::axum::response::IntoResponse::into_response(
1701                                            super::validation::malformed_parameter(#location)
1702                                        ),
1703                                    },
1704                                    Err(_) => return ::axum::response::IntoResponse::into_response(
1705                                        super::validation::malformed_parameter(#location)
1706                                    ),
1707                                },
1708                                (None, _) => None,
1709                                _ => return ::axum::response::IntoResponse::into_response(
1710                                    super::validation::malformed_parameter(#location)
1711                                ),
1712                            };
1713                        });
1714                        if let Some(target) = target {
1715                            parameter_decode_checks.push(quote! {
1716                                if let Some(value) = &#ident {
1717                                    if let Err(rejection) = super::validation::validate_parameter(
1718                                        #target, #location, value,
1719                                    ) {
1720                                        return ::axum::response::IntoResponse::into_response(rejection);
1721                                    }
1722                                }
1723                            });
1724                        }
1725                        call_args.push(quote! { #ident });
1726                    }
1727                    continue;
1728                }
1729                if p.required {
1730                    if let Some(target) = target {
1731                        parameter_decode_checks.push(quote! {
1732                            let mut __values = __headers.get_all(#wire).iter();
1733                            let #ident: #ty = match (__values.next(), __values.next()) {
1734                                (Some(value), None) => match value.to_str() {
1735                                    Ok(raw) => match super::validation::decode_parameter(
1736                                        raw, #target, #location, #string_wire,
1737                                    ) {
1738                                        Ok(value) => value,
1739                                        Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1740                                    },
1741                                    Err(_) => return ::axum::response::IntoResponse::into_response(
1742                                        super::validation::malformed_parameter(#location)
1743                                    ),
1744                                },
1745                                (None, _) => return ::axum::response::IntoResponse::into_response(
1746                                    super::validation::missing_parameter(#location)
1747                                ),
1748                                _ => return ::axum::response::IntoResponse::into_response(
1749                                    super::validation::malformed_parameter(#location)
1750                                ),
1751                            };
1752                        });
1753                    } else {
1754                        parameter_decode_checks.push(quote! {
1755                            let #ident: #ty = match __headers.get(#wire).and_then(|v| v.to_str().ok()) {
1756                                Some(raw) => match __decode_query_scalar(raw, #wire) {
1757                                    Ok(value) => value,
1758                                    Err(_) => return ::axum::http::StatusCode::BAD_REQUEST.into_response(),
1759                                },
1760                                None => return ::axum::http::StatusCode::BAD_REQUEST.into_response(),
1761                            };
1762                        });
1763                    }
1764                    call_args.push(quote! { #ident });
1765                } else {
1766                    if let Some(target) = target {
1767                        parameter_decode_checks.push(quote! {
1768                            let mut __values = __headers.get_all(#wire).iter();
1769                            let #ident: ::std::option::Option<#ty> = match (__values.next(), __values.next()) {
1770                                (Some(value), None) => match value.to_str() {
1771                                    Ok(raw) => match super::validation::decode_parameter(raw, #target, #location, #string_wire) {
1772                                        Ok(value) => Some(value),
1773                                        Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1774                                    },
1775                                    Err(_) => return ::axum::response::IntoResponse::into_response(
1776                                        super::validation::malformed_parameter(#location)
1777                                    ),
1778                                },
1779                                (None, _) => None,
1780                                _ => return ::axum::response::IntoResponse::into_response(
1781                                    super::validation::malformed_parameter(#location)
1782                                ),
1783                            };
1784                        });
1785                    } else {
1786                        parameter_decode_checks.push(quote! {
1787                            let #ident: ::std::option::Option<#ty> = __headers.get(#wire)
1788                                .and_then(|value| value.to_str().ok())
1789                                .and_then(|raw| __decode_query_scalar(raw, #wire).ok());
1790                        });
1791                    }
1792                    call_args.push(quote! { #ident });
1793                }
1794            }
1795        }
1796        if !cookie_params.is_empty() {
1797            parameter_decode_checks.push(quote! {
1798                let mut __cookies = match super::validation::parse_cookies(&__headers) {
1799                    Ok(cookies) => cookies,
1800                    Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1801                };
1802            });
1803            for p in &cookie_params {
1804                let wire = p.name.as_str();
1805                let ident = self.parameter_ident(p);
1806                let ty = self.query_parameter_type(p);
1807                let location = parameter_location("cookie", wire);
1808                let target = self
1809                    .validation_target(validation_bundle, op, "cookie", Some(wire))?
1810                    .ok_or_else(|| {
1811                        ServerCodegenError::Validation(format!(
1812                            "cookie extraction requires validation for operation `{}`",
1813                            op.operation_id
1814                        ))
1815                    })?;
1816                let string_wire = self.parameter_schema_is_string(p);
1817                if p.required {
1818                    parameter_decode_checks.push(quote! {
1819                        let #ident: #ty = match __cookies.remove(#wire) {
1820                            Some(raw) => match super::validation::decode_parameter(&raw, #target, #location, #string_wire) {
1821                                Ok(value) => value,
1822                                Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1823                            },
1824                            None => return ::axum::response::IntoResponse::into_response(
1825                                super::validation::missing_parameter(#location)
1826                            ),
1827                        };
1828                    });
1829                    call_args.push(quote! { #ident });
1830                } else {
1831                    parameter_decode_checks.push(quote! {
1832                        let #ident: ::std::option::Option<#ty> = match __cookies.remove(#wire) {
1833                            Some(raw) => match super::validation::decode_parameter(&raw, #target, #location, #string_wire) {
1834                                Ok(value) => Some(value),
1835                                Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1836                            },
1837                            None => None,
1838                        };
1839                    });
1840                    call_args.push(quote! { #ident });
1841                }
1842            }
1843        }
1844
1845        // Body
1846        let mut body_decode = TokenStream::new();
1847        let body_ty_opt = body_type(op);
1848        if let Some(body_ty) = &body_ty_opt {
1849            let body_ty_tokens = self.model_type(body_ty);
1850            let transport_body = match &op.request_body {
1851                Some(RequestBodyContent::OctetStream { media_type }) => {
1852                    Some((format_ident!("decode_binary_body"), media_type.clone()))
1853                }
1854                Some(RequestBodyContent::Binary { media_type }) => {
1855                    Some((format_ident!("decode_binary_body"), media_type.clone()))
1856                }
1857                Some(RequestBodyContent::TextPlain { media_type }) => {
1858                    Some((format_ident!("decode_text_body"), media_type.clone()))
1859                }
1860                _ => None,
1861            };
1862            let validated_json = matches!(&op.request_body, Some(RequestBodyContent::Json { .. }))
1863                && validation_bundle.is_some();
1864            let validated_form = matches!(
1865                &op.request_body,
1866                Some(RequestBodyContent::FormUrlEncoded { .. })
1867            ) && validation_bundle.is_some();
1868            let typed_multipart =
1869                matches!(&op.request_body, Some(RequestBodyContent::Multipart { .. }));
1870            if let Some((decoder, media_type)) = transport_body {
1871                extractors.push(quote! { __request: ::axum::extract::Request });
1872                let required = op.request_body_required;
1873                let max_body_bytes = self.server.validation.max_body_bytes;
1874                body_decode = quote! {
1875                    let body: ::std::option::Option<#body_ty_tokens> =
1876                        match super::validation::#decoder(
1877                            __request,
1878                            #media_type,
1879                            #required,
1880                            #max_body_bytes,
1881                        ).await {
1882                            Ok(body) => body,
1883                            Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1884                        };
1885                };
1886                if required {
1887                    body_decode.extend(quote! {
1888                        let body = match body {
1889                            Some(body) => body,
1890                            None => return ::axum::response::IntoResponse::into_response(
1891                                super::validation::generated_contract_error()
1892                            ),
1893                        };
1894                    });
1895                }
1896                call_args.push(quote! { body });
1897            } else if typed_multipart {
1898                extractors.push(quote! { __request: ::axum::extract::Request });
1899                let Some(RequestBodyContent::Multipart {
1900                    validation_schema, ..
1901                }) = &op.request_body
1902                else {
1903                    return Err(ServerCodegenError::Internal(
1904                        "typed multipart body lost its schema".to_string(),
1905                    ));
1906                };
1907                let plans = self.multipart_field_plans(validation_schema)?;
1908                let multipart_validation = self
1909                    .validation_target(validation_bundle, op, "body", None)?
1910                    .map(|target| {
1911                        quote! {
1912                            if let Err(rejection) = super::validation::validate_parameter(
1913                                #target,
1914                                "/body",
1915                                &::serde_json::Value::Object(validation_object),
1916                            ) {
1917                                return ::axum::response::IntoResponse::into_response(rejection);
1918                            }
1919                        }
1920                    })
1921                    .unwrap_or_default();
1922                let mut binary_locals = Vec::new();
1923                let mut binary_validation_arms = Vec::new();
1924                let mut binary_patches = Vec::new();
1925                for plan in plans
1926                    .iter()
1927                    .filter(|plan| matches!(plan.kind, MultipartFieldKind::Binary))
1928                {
1929                    let wire_name = &plan.wire_name;
1930                    let field_ident = &plan.field_ident;
1931                    let slot = format_ident!("__multipart_binary_{}", field_ident);
1932                    binary_locals.push(quote! {
1933                        let mut #slot: ::std::option::Option<::bytes::Bytes> = None;
1934                    });
1935                    binary_validation_arms.push(quote! {
1936                        #wire_name => ::serde_json::Value::String(
1937                            "x".repeat(#slot.as_ref().map_or(0, ::bytes::Bytes::len))
1938                        ),
1939                    });
1940                    binary_patches.push(match (self.config.types.binary, plan.required) {
1941                        (crate::type_mapping::BinaryStrategy::Bytes, true) => quote! {
1942                            body.#field_ident = match #slot {
1943                                Some(bytes) => bytes,
1944                                None => return ::axum::response::IntoResponse::into_response(
1945                                    (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "missing required multipart field")
1946                                ),
1947                            };
1948                        },
1949                        (crate::type_mapping::BinaryStrategy::Bytes, false) => quote! {
1950                            body.#field_ident = #slot;
1951                        },
1952                        (crate::type_mapping::BinaryStrategy::VecU8, true) => quote! {
1953                            body.#field_ident = match #slot {
1954                                Some(bytes) => bytes.to_vec(),
1955                                None => return ::axum::response::IntoResponse::into_response(
1956                                    (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "missing required multipart field")
1957                                ),
1958                            };
1959                        },
1960                        (crate::type_mapping::BinaryStrategy::VecU8, false) => quote! {
1961                            body.#field_ident = #slot.map(|bytes| bytes.to_vec());
1962                        },
1963                        (crate::type_mapping::BinaryStrategy::String, _) => unreachable!(
1964                            "string-backed binary fields must use multipart text extraction"
1965                        ),
1966                    });
1967                }
1968                let mut field_arms = Vec::new();
1969                for plan in plans {
1970                    let wire_name = plan.wire_name;
1971                    let binary_slot = format_ident!("__multipart_binary_{}", plan.field_ident);
1972                    let decode = match plan.kind {
1973                        MultipartFieldKind::Binary => quote! {
1974                            let bytes = match field.bytes().await {
1975                                Ok(bytes) => bytes,
1976                                Err(_) => return ::axum::response::IntoResponse::into_response(
1977                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart field")
1978                                ),
1979                            };
1980                            #binary_slot = Some(bytes);
1981                            ::serde_json::Value::Array(Vec::new())
1982                        },
1983                        MultipartFieldKind::String => quote! {
1984                            match field.text().await {
1985                                Ok(text) => ::serde_json::Value::String(text),
1986                                Err(_) => return ::axum::response::IntoResponse::into_response(
1987                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart text field")
1988                                ),
1989                            }
1990                        },
1991                        MultipartFieldKind::Integer => quote! {
1992                            let text = match field.text().await {
1993                                Ok(text) => text,
1994                                Err(_) => return ::axum::response::IntoResponse::into_response(
1995                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart integer field")
1996                                ),
1997                            };
1998                            match text.parse::<i64>() {
1999                                Ok(value) => ::serde_json::Value::Number(value.into()),
2000                                Err(_) => return ::axum::response::IntoResponse::into_response(
2001                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart integer field")
2002                                ),
2003                            }
2004                        },
2005                        MultipartFieldKind::UnsignedInteger => quote! {
2006                            let text = match field.text().await {
2007                                Ok(text) => text,
2008                                Err(_) => return ::axum::response::IntoResponse::into_response(
2009                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart unsigned integer field")
2010                                ),
2011                            };
2012                            match text.parse::<u64>() {
2013                                Ok(value) => ::serde_json::Value::Number(value.into()),
2014                                Err(_) => return ::axum::response::IntoResponse::into_response(
2015                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart unsigned integer field")
2016                                ),
2017                            }
2018                        },
2019                        MultipartFieldKind::Number => quote! {
2020                            let text = match field.text().await {
2021                                Ok(text) => text,
2022                                Err(_) => return ::axum::response::IntoResponse::into_response(
2023                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart number field")
2024                                ),
2025                            };
2026                            match text.parse::<f64>().ok().and_then(::serde_json::Number::from_f64) {
2027                                Some(value) => ::serde_json::Value::Number(value),
2028                                None => return ::axum::response::IntoResponse::into_response(
2029                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart number field")
2030                                ),
2031                            }
2032                        },
2033                        MultipartFieldKind::Boolean => quote! {
2034                            let text = match field.text().await {
2035                                Ok(text) => text,
2036                                Err(_) => return ::axum::response::IntoResponse::into_response(
2037                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart boolean field")
2038                                ),
2039                            };
2040                            match text.parse::<bool>() {
2041                                Ok(value) => ::serde_json::Value::Bool(value),
2042                                Err(_) => return ::axum::response::IntoResponse::into_response(
2043                                    (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart boolean field")
2044                                ),
2045                            }
2046                        },
2047                    };
2048                    field_arms.push(quote! {
2049                        #wire_name => { #decode }
2050                    });
2051                }
2052                let required = op.request_body_required;
2053                body_decode = quote! {
2054                    let __has_multipart_content_type = __request.headers()
2055                        .get(::axum::http::header::CONTENT_TYPE)
2056                        .is_some();
2057                    let body: ::std::option::Option<#body_ty_tokens> =
2058                        if !__has_multipart_content_type && !#required {
2059                            None
2060                        } else {
2061                            let mut multipart = match <::axum::extract::Multipart as ::axum::extract::FromRequest<T>>::from_request(__request, &api).await {
2062                                Ok(multipart) => multipart,
2063                                Err(_) => return ::axum::response::IntoResponse::into_response(
2064                                    (::axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE, "invalid multipart request")
2065                                ),
2066                            };
2067                            let mut object = ::serde_json::Map::new();
2068                            let mut validation_object = ::serde_json::Map::new();
2069                            #(#binary_locals)*
2070                            loop {
2071                                let field = match multipart.next_field().await {
2072                                    Ok(Some(field)) => field,
2073                                    Ok(None) => break,
2074                                    Err(_) => return ::axum::response::IntoResponse::into_response(
2075                                        (::axum::http::StatusCode::BAD_REQUEST, "malformed multipart request")
2076                                    ),
2077                                };
2078                                let name = match field.name() {
2079                                    Some(name) => name.to_string(),
2080                                    None => return ::axum::response::IntoResponse::into_response(
2081                                        (::axum::http::StatusCode::BAD_REQUEST, "multipart field has no name")
2082                                    ),
2083                                };
2084                                if object.contains_key(&name) {
2085                                    return ::axum::response::IntoResponse::into_response(
2086                                        (::axum::http::StatusCode::BAD_REQUEST, "duplicate multipart field")
2087                                    );
2088                                }
2089                                let value = match name.as_str() {
2090                                    #(#field_arms,)*
2091                                    _ => return ::axum::response::IntoResponse::into_response(
2092                                        (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "unknown multipart field")
2093                                    ),
2094                                };
2095                                let validation_value = match name.as_str() {
2096                                    #(#binary_validation_arms)*
2097                                    _ => value.clone(),
2098                                };
2099                                object.insert(name.clone(), value);
2100                                validation_object.insert(name, validation_value);
2101                            }
2102                            #multipart_validation
2103                            match ::serde_json::from_value::<#body_ty_tokens>(::serde_json::Value::Object(object)) {
2104                                Ok(mut body) => {
2105                                    #(#binary_patches)*
2106                                    Some(body)
2107                                },
2108                                Err(_) => return ::axum::response::IntoResponse::into_response(
2109                                    (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "invalid multipart body")
2110                                ),
2111                            }
2112                        };
2113                };
2114                if required {
2115                    body_decode.extend(quote! {
2116                        let body = match body {
2117                            Some(body) => body,
2118                            None => return ::axum::response::IntoResponse::into_response(
2119                                (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "missing required multipart body")
2120                            ),
2121                        };
2122                    });
2123                }
2124                call_args.push(quote! { body });
2125            } else if validated_json {
2126                extractors.push(quote! { __request: ::axum::extract::Request });
2127                let Some(RequestBodyContent::Json { media_type, .. }) = &op.request_body else {
2128                    return Err(ServerCodegenError::Internal(
2129                        "validated JSON body lost its media type".to_string(),
2130                    ));
2131                };
2132                let target = self
2133                    .validation_target(validation_bundle, op, "body", None)?
2134                    .ok_or_else(|| {
2135                        ServerCodegenError::Validation(format!(
2136                            "validation target unexpectedly disabled for operation `{}` body",
2137                            op.operation_id
2138                        ))
2139                    })?;
2140                let required = op.request_body_required;
2141                let max_body_bytes = self.server.validation.max_body_bytes;
2142                body_decode = if required {
2143                    quote! {
2144                        let body: #body_ty_tokens = match super::validation::decode_json_body::<#body_ty_tokens>(
2145                            __request,
2146                            #target,
2147                            #media_type,
2148                            true,
2149                            #max_body_bytes,
2150                        ).await {
2151                            Ok(Some(body)) => body,
2152                            Ok(None) => return ::axum::response::IntoResponse::into_response(
2153                                super::validation::generated_contract_error()
2154                            ),
2155                            Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
2156                        };
2157                    }
2158                } else {
2159                    quote! {
2160                        let body: ::std::option::Option<#body_ty_tokens> =
2161                            match super::validation::decode_json_body::<#body_ty_tokens>(
2162                                __request,
2163                                #target,
2164                                #media_type,
2165                                false,
2166                                #max_body_bytes,
2167                            ).await {
2168                                Ok(body) => body,
2169                                Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
2170                            };
2171                    }
2172                };
2173                call_args.push(quote! { body });
2174            } else if validated_form {
2175                extractors.push(quote! { __request: ::axum::extract::Request });
2176                let Some(RequestBodyContent::FormUrlEncoded { media_type, .. }) = &op.request_body
2177                else {
2178                    return Err(ServerCodegenError::Internal(
2179                        "validated form body lost its media type".to_string(),
2180                    ));
2181                };
2182                let target = self
2183                    .validation_target(validation_bundle, op, "body", None)?
2184                    .ok_or_else(|| {
2185                        ServerCodegenError::Validation(format!(
2186                            "validation target unexpectedly disabled for operation `{}` body",
2187                            op.operation_id
2188                        ))
2189                    })?;
2190                let allowed_fields = self.form_field_names(op)?;
2191                let required_fields = self.form_required_field_names(op)?;
2192                let required = op.request_body_required;
2193                let max_body_bytes = self.server.validation.max_body_bytes;
2194                body_decode = quote! {
2195                    let body: ::std::option::Option<#body_ty_tokens> =
2196                        match super::validation::decode_form_body::<#body_ty_tokens>(
2197                            __request,
2198                            #target,
2199                            #media_type,
2200                            #required,
2201                            #max_body_bytes,
2202                            &[#(#allowed_fields),*],
2203                            &[#(#required_fields),*],
2204                        ).await {
2205                            Ok(body) => body,
2206                            Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
2207                        };
2208                };
2209                if required {
2210                    body_decode.extend(quote! {
2211                        let body = match body {
2212                            Some(body) => body,
2213                            None => return ::axum::response::IntoResponse::into_response(
2214                                super::validation::generated_contract_error()
2215                            ),
2216                        };
2217                    });
2218                }
2219                call_args.push(quote! { body });
2220            } else if op.request_body_required {
2221                extractors.push(quote! {
2222                    ::axum::Json(body): ::axum::Json<#body_ty_tokens>
2223                });
2224                call_args.push(quote! { body });
2225            } else {
2226                extractors.push(quote! {
2227                    body: ::std::option::Option<::axum::Json<#body_ty_tokens>>
2228                });
2229                call_args.push(quote! { body.map(|::axum::Json(b)| b) });
2230            }
2231        }
2232
2233        // Keep referencing trait_ident so the where-bound name is
2234        // visible to downstream readers — clippy would otherwise flag
2235        // it as unused in some configurations.
2236        let _ = trait_ident;
2237        let raw_query_validation = (!raw_query_validation_checks.is_empty()).then(|| {
2238            quote! {
2239                if let Some(raw) = __raw_query.as_deref() {
2240                    if __validate_urlencoded(raw).is_err() {
2241                        return ::axum::response::IntoResponse::into_response(
2242                            super::validation::malformed_parameter("/query")
2243                        );
2244                    }
2245                }
2246                let __raw_query_pairs = __query_pairs(__raw_query.as_deref());
2247                #(#raw_query_validation_checks)*
2248            }
2249        });
2250
2251        // Handler returns `axum::response::Response` so the required-
2252        // param short-circuit (400 BadRequest) and the trait method's
2253        // typed response enum (via IntoResponse) can both flow out
2254        // through the same return type.
2255        Ok(quote! {
2256            async fn #handler_ident<T>(
2257                #(#extractors),*
2258            ) -> ::axum::response::Response
2259            where
2260                T: super::api::#trait_ident + Clone + Send + Sync + 'static,
2261            {
2262                #path_decode
2263                #raw_query_validation
2264                #query_decode
2265                #(#required_query_checks)*
2266                #(#query_validation_checks)*
2267                #(#parameter_decode_checks)*
2268                #body_decode
2269                ::axum::response::IntoResponse::into_response(
2270                    api.#trait_method(#(#call_args),*).await,
2271                )
2272            }
2273        })
2274    }
2275
2276    fn emit_api(
2277        &self,
2278        groups: &BTreeMap<String, Vec<&OperationInfo>>,
2279        response_enum_names: &BTreeMap<String, syn::Ident>,
2280    ) -> TokenStream {
2281        let provenance_attribute = self.provenance_attribute();
2282        let traits: Vec<TokenStream> = groups
2283            .iter()
2284            .map(|(tag, ops)| self.emit_trait(tag, ops, response_enum_names))
2285            .collect();
2286
2287        // Inline string enums declared on parameters get synthetic
2288        // type names (e.g. `ListInputItemsOrder`). The analyzer
2289        // surfaces enum_values; we emit the enum here so the trait
2290        // signature compiles. Dedup by name in case two ops in the
2291        // same picked set share the same synthetic name.
2292        let mut emitted: std::collections::BTreeSet<String> = Default::default();
2293        let mut param_enums: Vec<TokenStream> = Vec::new();
2294        for op in groups.values().flatten() {
2295            for p in &op.parameters {
2296                if let Some(values) = &p.enum_values {
2297                    if emitted.insert(p.rust_type.clone()) {
2298                        param_enums.push(emit_param_enum(&p.rust_type, values));
2299                    }
2300                }
2301            }
2302        }
2303
2304        quote! {
2305            //! Per-tag traits. Implement one of these on your own
2306            //! struct; the router (P5) wires it into axum.
2307
2308            #provenance_attribute
2309
2310            #![allow(clippy::too_many_arguments)]
2311
2312            use super::errors::*;
2313            // Schemas live in `<parent>/types.rs`. Reaching them via
2314            // `super::super::types::*` instead of a glob on the
2315            // parent module keeps these imports stable regardless of
2316            // how the user mounts the generated tree.
2317            #[allow(unused_imports)]
2318            use super::super::types::*;
2319
2320            #(#param_enums)*
2321
2322            #(#traits)*
2323        }
2324    }
2325
2326    fn emit_trait(
2327        &self,
2328        tag: &str,
2329        ops: &[&OperationInfo],
2330        response_enum_names: &BTreeMap<String, syn::Ident>,
2331    ) -> TokenStream {
2332        let trait_ident = trait_ident_for_tag(tag);
2333        let methods: Vec<TokenStream> = ops
2334            .iter()
2335            .map(|op| self.emit_method_sig(op, response_enum_names))
2336            .collect();
2337        let doc = format!(" Operations under the `{tag}` tag.");
2338        quote! {
2339            #[doc = #doc]
2340            #[async_trait::async_trait]
2341            pub trait #trait_ident: Send + Sync + 'static {
2342                #(#methods)*
2343            }
2344        }
2345    }
2346
2347    fn emit_method_sig(
2348        &self,
2349        op: &OperationInfo,
2350        response_enum_names: &BTreeMap<String, syn::Ident>,
2351    ) -> TokenStream {
2352        let name = format_ident!("{}", op.operation_id.to_snake_case());
2353        let response_ty = &response_enum_names[&op.operation_id];
2354
2355        // Order: path → query → header → body. Required params keep
2356        // their declared rust_type; optional params wrap in Option<…>.
2357        // This mirrors what the router handler extracts so positional
2358        // ordering matches the call site exactly.
2359        let mut params: Vec<TokenStream> = Vec::new();
2360        for p in &op.parameters {
2361            if p.location == "path" {
2362                let ident = self.parameter_ident(p);
2363                let ty = self.query_parameter_type(p);
2364                params.push(quote! { #ident: #ty });
2365            }
2366        }
2367        for p in &op.parameters {
2368            if p.location == "query" {
2369                let ident = self.parameter_ident(p);
2370                let ty = self.query_parameter_type(p);
2371                // Required query params land as `T`; the handler
2372                // validates presence and returns 400 if absent, so
2373                // by the time the trait method sees the value it
2374                // must be Some. Optional → `Option<T>`.
2375                if p.required {
2376                    params.push(quote! { #ident: #ty });
2377                } else {
2378                    params.push(quote! { #ident: ::std::option::Option<#ty> });
2379                }
2380            }
2381        }
2382        for p in &op.parameters {
2383            if p.location == "header" {
2384                let ident = self.parameter_ident(p);
2385                let ty = self.query_parameter_type(p);
2386                if p.required {
2387                    params.push(quote! { #ident: #ty });
2388                } else {
2389                    params.push(quote! { #ident: ::std::option::Option<#ty> });
2390                }
2391            }
2392        }
2393        for p in &op.parameters {
2394            if p.location == "cookie" {
2395                let ident = self.parameter_ident(p);
2396                let ty = self.query_parameter_type(p);
2397                if p.required {
2398                    params.push(quote! { #ident: #ty });
2399                } else {
2400                    params.push(quote! { #ident: ::std::option::Option<#ty> });
2401                }
2402            }
2403        }
2404        if let Some(body) = body_type(op) {
2405            let body_ty = self.model_type(&body);
2406            if op.request_body_required {
2407                params.push(quote! { body: #body_ty });
2408            } else {
2409                params.push(quote! { body: Option<#body_ty> });
2410            }
2411        }
2412
2413        let summary_doc = op
2414            .summary
2415            .as_deref()
2416            .map(|s| format!(" {s}"))
2417            .unwrap_or_default();
2418        let route_doc = format!(" `{} {}`", op.method, op.path);
2419
2420        quote! {
2421            #[doc = #summary_doc]
2422            #[doc = ""]
2423            #[doc = #route_doc]
2424            async fn #name(&self, #(#params),*) -> #response_ty;
2425        }
2426    }
2427
2428    /// Per-op `<Op>Query` struct emitted into router.rs when the op
2429    /// has any query parameters. An operation-specific decoder fills it from
2430    /// Axum's raw query so repeated and structured keys remain observable.
2431    fn emit_query_struct(&self, op: &OperationInfo) -> Option<TokenStream> {
2432        let query_params: Vec<&_> = op
2433            .parameters
2434            .iter()
2435            .filter(|p| p.location == "query")
2436            .collect();
2437        if query_params.is_empty() {
2438            return None;
2439        }
2440        let ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
2441        let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
2442        let mut fields = Vec::new();
2443        let mut decoders = Vec::new();
2444        let mut field_idents = Vec::new();
2445        for parameter in query_params {
2446            let field_ident = self.parameter_ident(parameter);
2447            let field_type = self.query_parameter_type(parameter);
2448            let wire_name = parameter.name.as_str();
2449            fields.push(quote! {
2450                pub #field_ident: ::std::option::Option<#field_type>
2451            });
2452            field_idents.push(field_ident.clone());
2453
2454            let decoder = match &parameter.query_serialization {
2455                Some(QuerySerialization::FormExplodedArray { item_type }) => {
2456                    if let crate::analysis::ArrayItemType::FlatStructRef { properties, .. } =
2457                        item_type
2458                    {
2459                        let property_names = properties
2460                            .iter()
2461                            .map(|property| property.wire_name.clone())
2462                            .collect::<Vec<_>>();
2463                        // AWS query-protocol flat structures arrive as
2464                        // `param.N.Prop=value`; group by N and decode each
2465                        // group as one JSON object so serde fills the struct.
2466                        quote! {
2467                            let #field_ident = {
2468                                let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2469                                let prefix = concat!(#wire_name, ".");
2470                                let mut groups: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2471                                let allowed = [#(#property_names),*];
2472                                for (key, value) in &__pairs {
2473                                    let Some(rest) = key.strip_prefix(prefix) else { continue };
2474                                    if let Some(index) = rest.strip_suffix("[]").and_then(|index| index.parse::<usize>().ok()) {
2475                                        groups.entry(index).or_default();
2476                                        continue;
2477                                    }
2478                                    let Some((index, property)) = rest.split_once('.') else { continue };
2479                                    let Ok(index) = index.parse::<usize>() else { continue };
2480                                    if !allowed.contains(&property) {
2481                                        continue;
2482                                    }
2483                                    groups
2484                                        .entry(index)
2485                                        .or_default()
2486                                        .insert(property.to_string(), ::serde_json::Value::String(value.clone()));
2487                                }
2488                                if empty_marker && !groups.is_empty() {
2489                                    return Err(format!(
2490                                        "query array `{}` cannot combine values with its empty marker",
2491                                        #wire_name,
2492                                    ));
2493                                }
2494                                if empty_marker {
2495                                    Some(Vec::new())
2496                                } else if groups.is_empty() {
2497                                    None
2498                                } else {
2499                                    let mut values = Vec::with_capacity(groups.len());
2500                                    for (_, object) in groups {
2501                                        values.push(
2502                                            ::serde_json::from_value(::serde_json::Value::Object(object))
2503                                                .map_err(|error| format!("invalid query structure for `{}`: {error}", #wire_name))?,
2504                                        );
2505                                    }
2506                                    Some(values)
2507                                }
2508                            };
2509                        }
2510                    } else if let crate::analysis::ArrayItemType::NestedStructRef {
2511                        properties,
2512                        ..
2513                    } = item_type
2514                    {
2515                        let scalar_json = |kind: crate::analysis::QueryScalarType| match kind {
2516                            crate::analysis::QueryScalarType::String => {
2517                                quote! { ::serde_json::Value::String(value.clone()) }
2518                            }
2519                            crate::analysis::QueryScalarType::Boolean => quote! {
2520                                ::serde_json::Value::Bool(value.parse::<bool>().map_err(|error| {
2521                                    format!("invalid boolean query value `{value}`: {error}")
2522                                })?)
2523                            },
2524                            crate::analysis::QueryScalarType::Integer
2525                            | crate::analysis::QueryScalarType::Number => quote! {
2526                                match ::serde_json::from_str::<::serde_json::Value>(value)
2527                                    .map_err(|error| format!("invalid numeric query value `{value}`: {error}"))?
2528                                {
2529                                    parsed @ ::serde_json::Value::Number(_) => parsed,
2530                                    _ => return Err(format!("invalid numeric query value `{value}`")),
2531                                }
2532                            },
2533                        };
2534                        let property_decoders = properties
2535                            .iter()
2536                            .enumerate()
2537                            .map(|(property_index, property)| {
2538                                let property_name = property.wire_name.as_str();
2539                                match &property.value_type {
2540                                    crate::analysis::QueryStructPropertyType::Scalar(kind) => {
2541                                        let parsed = scalar_json(*kind);
2542                                        quote! {
2543                                            for (key, value) in &__pairs {
2544                                                let Some(rest) = key.strip_prefix(prefix) else { continue };
2545                                                let Some((index, tail)) = rest.split_once('.') else { continue };
2546                                                let Ok(index) = index.parse::<usize>() else { continue };
2547                                                if tail != #property_name { continue; }
2548                                                let parsed = #parsed;
2549                                                groups.entry(index).or_default()
2550                                                    .insert(#property_name.to_string(), parsed);
2551                                            }
2552                                        }
2553                                    }
2554                                    crate::analysis::QueryStructPropertyType::Object { properties } => {
2555                                        let nested_groups = format_ident!("nested_objects_{property_index}");
2556                                        let leaf_parsers = properties.iter().map(|leaf_property| {
2557                                            let leaf = leaf_property.wire_name.as_str();
2558                                            let crate::analysis::QueryStructPropertyType::Scalar(kind) = leaf_property.value_type else {
2559                                                unreachable!("flat query object cannot contain nested values");
2560                                            };
2561                                            let parsed = scalar_json(kind);
2562                                            quote! { #leaf => #parsed, }
2563                                        }).collect::<Vec<_>>();
2564                                        quote! {
2565                                            let mut #nested_groups: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2566                                            for (key, value) in &__pairs {
2567                                                let Some(rest) = key.strip_prefix(prefix) else { continue };
2568                                                let Some((index, tail)) = rest.split_once('.') else { continue };
2569                                                let Ok(index) = index.parse::<usize>() else { continue };
2570                                                if tail == concat!(#property_name, "[]") {
2571                                                    groups.entry(index).or_default().insert(
2572                                                        #property_name.to_string(),
2573                                                        ::serde_json::Value::Object(::serde_json::Map::new()),
2574                                                    );
2575                                                    continue;
2576                                                }
2577                                                let Some(leaf) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2578                                                let parsed = match leaf { #(#leaf_parsers)* _ => continue };
2579                                                #nested_groups.entry(index).or_default().insert(leaf.to_string(), parsed);
2580                                            }
2581                                            for (index, value) in #nested_groups {
2582                                                groups.entry(index).or_default().insert(#property_name.to_string(), ::serde_json::Value::Object(value));
2583                                            }
2584                                        }
2585                                    }
2586                                    crate::analysis::QueryStructPropertyType::Array { item_type } => {
2587                                        let nested_groups = format_ident!("nested_groups_{property_index}");
2588                                        match item_type {
2589                                            crate::analysis::ArrayItemType::Scalar(rust_type) => {
2590                                                let kind = if rust_type == "String" {
2591                                                    crate::analysis::QueryScalarType::String
2592                                                } else if rust_type == "bool" {
2593                                                    crate::analysis::QueryScalarType::Boolean
2594                                                } else if rust_type.starts_with('i') || rust_type.starts_with('u') {
2595                                                    crate::analysis::QueryScalarType::Integer
2596                                                } else {
2597                                                    crate::analysis::QueryScalarType::Number
2598                                                };
2599                                                let parsed = scalar_json(kind);
2600                                                quote! {
2601                                                    let mut #nested_groups: ::std::collections::BTreeMap<usize, ::std::collections::BTreeMap<usize, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2602                                                    for (key, value) in &__pairs {
2603                                                        let Some(rest) = key.strip_prefix(prefix) else { continue };
2604                                                        let Some((index, tail)) = rest.split_once('.') else { continue };
2605                                                let Ok(index) = index.parse::<usize>() else { continue };
2606                                                if tail == concat!(#property_name, "[]") {
2607                                                    groups.entry(index).or_default().insert(
2608                                                        #property_name.to_string(),
2609                                                        ::serde_json::Value::Array(Vec::new()),
2610                                                    );
2611                                                    continue;
2612                                                }
2613                                                let Some(nested_index) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2614                                                        let Ok(nested_index) = nested_index.parse::<usize>() else { continue };
2615                                                        let parsed = #parsed;
2616                                                        #nested_groups.entry(index).or_default().insert(nested_index, parsed);
2617                                                    }
2618                                                    for (index, values) in #nested_groups {
2619                                                        groups.entry(index).or_default().insert(
2620                                                            #property_name.to_string(),
2621                                                            ::serde_json::Value::Array(values.into_values().collect()),
2622                                                        );
2623                                                    }
2624                                                }
2625                                            }
2626                                            crate::analysis::ArrayItemType::SchemaRef(_) => quote! {
2627                                                let mut #nested_groups: ::std::collections::BTreeMap<usize, ::std::collections::BTreeMap<usize, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2628                                                for (key, value) in &__pairs {
2629                                                    let Some(rest) = key.strip_prefix(prefix) else { continue };
2630                                                    let Some((index, tail)) = rest.split_once('.') else { continue };
2631                                                let Ok(index) = index.parse::<usize>() else { continue };
2632                                                if tail == concat!(#property_name, "[]") {
2633                                                    groups.entry(index).or_default().insert(
2634                                                        #property_name.to_string(),
2635                                                        ::serde_json::Value::Array(Vec::new()),
2636                                                    );
2637                                                    continue;
2638                                                }
2639                                                let Some(nested_index) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2640                                                    let Ok(nested_index) = nested_index.parse::<usize>() else { continue };
2641                                                    #nested_groups.entry(index).or_default().insert(
2642                                                        nested_index,
2643                                                        ::serde_json::Value::String(value.clone()),
2644                                                    );
2645                                                }
2646                                                for (index, values) in #nested_groups {
2647                                                    groups.entry(index).or_default().insert(
2648                                                        #property_name.to_string(),
2649                                                        ::serde_json::Value::Array(values.into_values().collect()),
2650                                                    );
2651                                                }
2652                                            },
2653                                            crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
2654                                                let allowed = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
2655                                                let leaf_kinds = properties.iter().map(|property| match property.value_type {
2656                                                    crate::analysis::QueryStructPropertyType::Scalar(kind) => kind,
2657                                                    crate::analysis::QueryStructPropertyType::Array { .. }
2658                                                    | crate::analysis::QueryStructPropertyType::Object { .. } => unreachable!("flat query struct cannot contain nested values"),
2659                                                }).collect::<Vec<_>>();
2660                                                let leaf_parsers = allowed.iter().zip(leaf_kinds).map(|(leaf, kind)| {
2661                                                    let parsed = scalar_json(kind);
2662                                                    quote! {
2663                                                        #leaf => #parsed,
2664                                                    }
2665                                                }).collect::<Vec<_>>();
2666                                                quote! {
2667                                                    let mut #nested_groups: ::std::collections::BTreeMap<usize, ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>>> = ::std::collections::BTreeMap::new();
2668                                                    for (key, value) in &__pairs {
2669                                                        let Some(rest) = key.strip_prefix(prefix) else { continue };
2670                                                        let Some((index, tail)) = rest.split_once('.') else { continue };
2671                                                let Ok(index) = index.parse::<usize>() else { continue };
2672                                                if tail == concat!(#property_name, "[]") {
2673                                                    groups.entry(index).or_default().insert(
2674                                                        #property_name.to_string(),
2675                                                        ::serde_json::Value::Array(Vec::new()),
2676                                                    );
2677                                                    continue;
2678                                                }
2679                                                let Some(tail) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2680                                                if let Some(nested_index) = tail.strip_suffix("[]").and_then(|index| index.parse::<usize>().ok()) {
2681                                                    #nested_groups.entry(index).or_default().entry(nested_index).or_default();
2682                                                    continue;
2683                                                }
2684                                                        let Some((nested_index, leaf)) = tail.split_once('.') else { continue };
2685                                                        let Ok(nested_index) = nested_index.parse::<usize>() else { continue };
2686                                                        let parsed = match leaf {
2687                                                            #(#leaf_parsers)*
2688                                                            _ => continue,
2689                                                        };
2690                                                        #nested_groups.entry(index).or_default()
2691                                                            .entry(nested_index).or_default()
2692                                                            .insert(leaf.to_string(), parsed);
2693                                                    }
2694                                                    for (index, values) in #nested_groups {
2695                                                        groups.entry(index).or_default().insert(
2696                                                            #property_name.to_string(),
2697                                                            ::serde_json::Value::Array(values.into_values().map(::serde_json::Value::Object).collect()),
2698                                                        );
2699                                                    }
2700                                                }
2701                                            }
2702                                            crate::analysis::ArrayItemType::NestedStructRef { .. } => unreachable!("analysis rejects query nesting deeper than two levels"),
2703                                        }
2704                                    }
2705                                }
2706                            })
2707                            .collect::<Vec<_>>();
2708                        quote! {
2709                            let #field_ident = {
2710                                let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2711                                let prefix = concat!(#wire_name, ".");
2712                                let mut groups: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2713                                for (key, _) in &__pairs {
2714                                    let Some(rest) = key.strip_prefix(prefix) else { continue };
2715                                    if let Some(index) = rest.strip_suffix("[]").and_then(|index| index.parse::<usize>().ok()) {
2716                                        groups.entry(index).or_default();
2717                                    }
2718                                }
2719                                #(#property_decoders)*
2720                                if empty_marker && !groups.is_empty() {
2721                                    return Err(format!(
2722                                        "query array `{}` cannot combine values with its empty marker",
2723                                        #wire_name,
2724                                    ));
2725                                }
2726                                if empty_marker {
2727                                    Some(Vec::new())
2728                                } else if groups.is_empty() {
2729                                    None
2730                                } else {
2731                                    let mut values = Vec::with_capacity(groups.len());
2732                                    for (_, object) in groups {
2733                                        values.push(
2734                                            ::serde_json::from_value(::serde_json::Value::Object(object))
2735                                                .map_err(|error| format!("invalid nested query structure for `{}`: {error}", #wire_name))?,
2736                                        );
2737                                    }
2738                                    Some(values)
2739                                }
2740                            };
2741                        }
2742                    } else {
2743                        quote! {
2744                            let #field_ident = {
2745                                let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2746                                let raw_values: Vec<&str> = __pairs
2747                                    .iter()
2748                                    .filter(|(key, _)| key == #wire_name)
2749                                    .map(|(_, value)| value.as_str())
2750                                    .collect();
2751                                if empty_marker && !raw_values.is_empty() {
2752                                    return Err(format!(
2753                                        "query array `{}` cannot combine values with its empty marker",
2754                                        #wire_name,
2755                                    ));
2756                                }
2757                                if empty_marker {
2758                                    Some(Vec::new())
2759                                } else if raw_values.is_empty() {
2760                                    None
2761                                } else {
2762                                    let mut values = Vec::with_capacity(raw_values.len());
2763                                    for raw in raw_values {
2764                                        values.push(__decode_query_scalar(raw, #wire_name)?);
2765                                    }
2766                                    Some(values)
2767                                }
2768                            };
2769                        }
2770                    }
2771                }
2772                Some(QuerySerialization::FormArray { .. }) => quote! {
2773                    let #field_ident = match (
2774                        __query_one(&__pairs, #wire_name)?,
2775                        __query_empty_marker(&__pairs, #wire_name)?,
2776                    ) {
2777                        (Some(_), true) => return Err(format!(
2778                            "query array `{}` cannot combine a value with its empty marker",
2779                            #wire_name,
2780                        )),
2781                        (Some(raw), false) => {
2782                            let mut values = Vec::new();
2783                            for item in raw.split(',') {
2784                                values.push(__decode_query_scalar(item, #wire_name)?);
2785                            }
2786                            Some(values)
2787                        }
2788                        (None, true) => Some(Vec::new()),
2789                        (None, false) => None,
2790                    };
2791                },
2792                Some(QuerySerialization::FormExplodedNestedObject { properties }) => {
2793                    let scalar_json = |kind: crate::analysis::QueryScalarType| match kind {
2794                        crate::analysis::QueryScalarType::String => {
2795                            quote! { ::serde_json::Value::String(value.clone()) }
2796                        }
2797                        crate::analysis::QueryScalarType::Boolean => quote! {
2798                            ::serde_json::Value::Bool(value.parse::<bool>().map_err(|error| format!("invalid boolean query value `{value}`: {error}"))?)
2799                        },
2800                        crate::analysis::QueryScalarType::Integer
2801                        | crate::analysis::QueryScalarType::Number => quote! {
2802                            match ::serde_json::from_str::<::serde_json::Value>(value)
2803                                .map_err(|error| format!("invalid numeric query value `{value}`: {error}"))?
2804                            {
2805                                parsed @ ::serde_json::Value::Number(_) => parsed,
2806                                _ => return Err(format!("invalid numeric query value `{value}`")),
2807                            }
2808                        },
2809                    };
2810                    let property_decoders = properties.iter().enumerate().map(|(property_index, property)| {
2811                        let property_name = property.wire_name.as_str();
2812                        match &property.value_type {
2813                            crate::analysis::QueryStructPropertyType::Scalar(kind) => {
2814                                let parsed = scalar_json(*kind);
2815                                quote! {
2816                                    if let Some((_, value)) = __pairs.iter().find(|(key, _)| key == concat!(#wire_name, ".", #property_name)) {
2817                                        let parsed = #parsed;
2818                                        object.insert(#property_name.to_string(), parsed);
2819                                    }
2820                                }
2821                            }
2822                            crate::analysis::QueryStructPropertyType::Object { properties } => {
2823                                let property_wire_name = format!("{wire_name}.{property_name}");
2824                                let leaf_parsers = properties.iter().map(|leaf_property| {
2825                                    let leaf = leaf_property.wire_name.as_str();
2826                                    let crate::analysis::QueryStructPropertyType::Scalar(kind) = leaf_property.value_type else {
2827                                        unreachable!("flat query object cannot contain nested values");
2828                                    };
2829                                    let parsed = scalar_json(kind);
2830                                    quote! { #leaf => #parsed, }
2831                                }).collect::<Vec<_>>();
2832                                quote! {
2833                                    let mut nested = ::serde_json::Map::new();
2834                                    for (key, value) in &__pairs {
2835                                        let Some(leaf) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2836                                        let parsed = match leaf { #(#leaf_parsers)* _ => continue };
2837                                        nested.insert(leaf.to_string(), parsed);
2838                                    }
2839                                    let nested_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2840                                    if nested_empty_marker && !nested.is_empty() {
2841                                        return Err(format!("query object `{}` cannot combine properties with its empty marker", #property_wire_name));
2842                                    }
2843                                    if nested_empty_marker || !nested.is_empty() {
2844                                        object.insert(#property_name.to_string(), ::serde_json::Value::Object(nested));
2845                                    }
2846                                }
2847                            }
2848                            crate::analysis::QueryStructPropertyType::Array { item_type } => {
2849                                let values_ident = format_ident!("property_values_{property_index}");
2850                                let property_wire_name = format!("{wire_name}.{property_name}");
2851                                match item_type {
2852                                    crate::analysis::ArrayItemType::Scalar(rust_type) => {
2853                                        let kind = if rust_type == "String" {
2854                                            crate::analysis::QueryScalarType::String
2855                                        } else if rust_type == "bool" {
2856                                            crate::analysis::QueryScalarType::Boolean
2857                                        } else if rust_type.starts_with('i') || rust_type.starts_with('u') {
2858                                            crate::analysis::QueryScalarType::Integer
2859                                        } else {
2860                                            crate::analysis::QueryScalarType::Number
2861                                        };
2862                                        let parsed = scalar_json(kind);
2863                                        quote! {
2864                                            let mut #values_ident: ::std::collections::BTreeMap<usize, ::serde_json::Value> = ::std::collections::BTreeMap::new();
2865                                            for (key, value) in &__pairs {
2866                                                let Some(index) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2867                                                let Ok(index) = index.parse::<usize>() else { continue };
2868                                                let parsed = #parsed;
2869                                                #values_ident.insert(index, parsed);
2870                                            }
2871                                            let property_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2872                                            if property_empty_marker && !#values_ident.is_empty() {
2873                                                return Err(format!("query array `{}` cannot combine values with its empty marker", #property_wire_name));
2874                                            }
2875                                            if property_empty_marker || !#values_ident.is_empty() {
2876                                                object.insert(#property_name.to_string(), ::serde_json::Value::Array(#values_ident.into_values().collect()));
2877                                            }
2878                                        }
2879                                    }
2880                                    crate::analysis::ArrayItemType::SchemaRef(_) => quote! {
2881                                        let mut #values_ident: ::std::collections::BTreeMap<usize, ::serde_json::Value> = ::std::collections::BTreeMap::new();
2882                                        for (key, value) in &__pairs {
2883                                            let Some(index) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2884                                            let Ok(index) = index.parse::<usize>() else { continue };
2885                                            #values_ident.insert(index, ::serde_json::Value::String(value.clone()));
2886                                        }
2887                                        let property_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2888                                        if property_empty_marker && !#values_ident.is_empty() {
2889                                            return Err(format!("query array `{}` cannot combine values with its empty marker", #property_wire_name));
2890                                        }
2891                                        if property_empty_marker || !#values_ident.is_empty() {
2892                                            object.insert(#property_name.to_string(), ::serde_json::Value::Array(#values_ident.into_values().collect()));
2893                                        }
2894                                    },
2895                                    crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
2896                                        let leaf_parsers = properties.iter().map(|leaf_property| {
2897                                            let leaf = leaf_property.wire_name.as_str();
2898                                            let crate::analysis::QueryStructPropertyType::Scalar(kind) = leaf_property.value_type else {
2899                                                unreachable!("flat query struct cannot contain arrays");
2900                                            };
2901                                            let parsed = scalar_json(kind);
2902                                            quote! { #leaf => #parsed, }
2903                                        }).collect::<Vec<_>>();
2904                                        quote! {
2905                                            let mut #values_ident: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2906                                            for (key, value) in &__pairs {
2907                                                let Some(tail) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2908                                                let Some((index, leaf)) = tail.split_once('.') else { continue };
2909                                                let Ok(index) = index.parse::<usize>() else { continue };
2910                                                let parsed = match leaf { #(#leaf_parsers)* _ => continue };
2911                                                #values_ident.entry(index).or_default().insert(leaf.to_string(), parsed);
2912                                            }
2913                                            let property_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2914                                            if property_empty_marker && !#values_ident.is_empty() {
2915                                                return Err(format!("query array `{}` cannot combine values with its empty marker", #property_wire_name));
2916                                            }
2917                                            if property_empty_marker || !#values_ident.is_empty() {
2918                                                object.insert(#property_name.to_string(), ::serde_json::Value::Array(
2919                                                    #values_ident.into_values().map(::serde_json::Value::Object).collect()
2920                                                ));
2921                                            }
2922                                        }
2923                                    }
2924                                    crate::analysis::ArrayItemType::NestedStructRef { .. } => unreachable!("analysis rejects query nesting deeper than two levels"),
2925                                }
2926                            }
2927                        }
2928                    }).collect::<Vec<_>>();
2929                    quote! {
2930                        let #field_ident = {
2931                            let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2932                            let mut object = ::serde_json::Map::new();
2933                            #(#property_decoders)*
2934                            if empty_marker && !object.is_empty() {
2935                                return Err(format!("query object `{}` cannot combine properties with its empty marker", #wire_name));
2936                            }
2937                            if object.is_empty() && !empty_marker {
2938                                None
2939                            } else {
2940                                Some(::serde_json::from_value(::serde_json::Value::Object(object))
2941                                    .map_err(|error| format!("invalid nested query object for `{}`: {error}", #wire_name))?)
2942                            }
2943                        };
2944                    }
2945                }
2946                Some(QuerySerialization::FormExplodedObject) => {
2947                    let property_names = self
2948                        .query_object_properties(parameter)
2949                        .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
2950                        .unwrap_or_default();
2951                    let required_names = self.query_object_required_properties(parameter);
2952                    quote! {
2953                        let #field_ident = {
2954                            let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2955                            let allowed = [#(#property_names),*];
2956                            let object_fields: Vec<(String, String)> = __pairs
2957                                .iter()
2958                                .filter(|(key, _)| allowed.contains(&key.as_str()))
2959                                .cloned()
2960                                .collect();
2961                            if empty_marker && !object_fields.is_empty() {
2962                                return Err(format!(
2963                                    "query object `{}` cannot combine properties with its empty marker",
2964                                    #wire_name,
2965                                ));
2966                            }
2967                            if object_fields.is_empty() && !empty_marker {
2968                                None
2969                            } else {
2970                                let required_properties: &[&str] = &[#(#required_names),*];
2971                                for required in required_properties {
2972                                    if !object_fields.iter().any(|(key, _)| key == required) {
2973                                        return Err(format!("query object `{}` is missing a required property", #wire_name));
2974                                    }
2975                                }
2976                                Some(__decode_query_object(&object_fields, #wire_name)?)
2977                            }
2978                        };
2979                    }
2980                }
2981                Some(QuerySerialization::FormObject) => {
2982                    let property_names = self
2983                        .query_object_properties(parameter)
2984                        .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
2985                        .unwrap_or_default();
2986                    let required_names = self.query_object_required_properties(parameter);
2987                    let has_required_names = !required_names.is_empty();
2988                    quote! {
2989                        let #field_ident = match (
2990                            __query_one(&__pairs, #wire_name)?,
2991                            __query_empty_marker(&__pairs, #wire_name)?,
2992                        ) {
2993                            (Some(_), true) => return Err(format!(
2994                                "query object `{}` cannot combine a value with its empty marker",
2995                                #wire_name,
2996                            )),
2997                            (Some(raw), false) => {
2998                                let parts: Vec<&str> = raw.split(',').collect();
2999                                if parts.len() % 2 != 0 {
3000                                    return Err(format!(
3001                                        "query object `{}` must contain alternating key,value entries",
3002                                        #wire_name,
3003                                    ));
3004                                }
3005                                let allowed = [#(#property_names),*];
3006                                let mut seen = ::std::collections::BTreeSet::new();
3007                                let object_fields: Vec<(String, String)> = parts
3008                                    .chunks_exact(2)
3009                                    .map(|pair| (pair[0].to_string(), pair[1].to_string()))
3010                                    .collect();
3011                                for (key, _) in &object_fields {
3012                                    if !allowed.contains(&key.as_str()) || !seen.insert(key.as_str()) {
3013                                        return Err(format!("query object `{}` has invalid properties", #wire_name));
3014                                    }
3015                                }
3016                                let required_properties: &[&str] = &[#(#required_names),*];
3017                                for required in required_properties {
3018                                    if !seen.contains(required) {
3019                                        return Err(format!("query object `{}` is missing a required property", #wire_name));
3020                                    }
3021                                }
3022                                Some(__decode_query_object(&object_fields, #wire_name)?)
3023                            }
3024                            (None, true) if #has_required_names => return Err(format!(
3025                                "query object `{}` is missing a required property",
3026                                #wire_name,
3027                            )),
3028                            (None, true) => Some(__decode_query_object(&[], #wire_name)?),
3029                            (None, false) => None,
3030                        };
3031                    }
3032                }
3033                Some(QuerySerialization::DeepObject) => {
3034                    let property_names = self
3035                        .query_object_properties(parameter)
3036                        .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
3037                        .unwrap_or_default();
3038                    let required_names = self.query_object_required_properties(parameter);
3039                    quote! {
3040                        let #field_ident = {
3041                            let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
3042                            let prefix = format!("{}[", #wire_name);
3043                            let allowed = [#(#property_names),*];
3044                            let mut object_fields = Vec::new();
3045                            for (key, value) in &__pairs {
3046                                if let Some(property) = key
3047                                    .strip_prefix(&prefix)
3048                                    .and_then(|rest| rest.strip_suffix(']'))
3049                                {
3050                                    if property.is_empty() {
3051                                        continue;
3052                                    }
3053                                    if !allowed.contains(&property) {
3054                                        return Err(format!(
3055                                            "unknown deepObject property `{}[{}]`",
3056                                            #wire_name,
3057                                            property,
3058                                        ));
3059                                    }
3060                                    object_fields.push((property.to_string(), value.clone()));
3061                                }
3062                            }
3063                            if empty_marker && !object_fields.is_empty() {
3064                                return Err(format!(
3065                                    "query object `{}` cannot combine properties with its empty marker",
3066                                    #wire_name,
3067                                ));
3068                            }
3069                            if object_fields.is_empty() && !empty_marker {
3070                                None
3071                            } else {
3072                                let required_properties: &[&str] = &[#(#required_names),*];
3073                                for required in required_properties {
3074                                    if !object_fields.iter().any(|(key, _)| key == required) {
3075                                        return Err(format!("query object `{}` is missing a required property", #wire_name));
3076                                    }
3077                                }
3078                                Some(__decode_query_object(&object_fields, #wire_name)?)
3079                            }
3080                        };
3081                    }
3082                }
3083                Some(
3084                    QuerySerialization::Unsupported { .. }
3085                    | QuerySerialization::SimpleHeaderArray { .. },
3086                )
3087                | None => quote! {
3088                    let #field_ident = __query_one(&__pairs, #wire_name)?
3089                        .map(|raw| __decode_query_scalar(&raw, #wire_name))
3090                        .transpose()?;
3091                },
3092            };
3093            decoders.push(decoder);
3094        }
3095        let doc = format!(
3096            " Query parameters for `{} {}` (operationId `{}`).",
3097            op.method, op.path, op.operation_id
3098        );
3099        Some(quote! {
3100            #[doc = #doc]
3101            #[derive(Debug, Default)]
3102            pub struct #ident {
3103                #(#fields),*
3104            }
3105
3106            fn #decode_ident(
3107                raw: ::std::option::Option<&str>,
3108            ) -> ::std::result::Result<#ident, String> {
3109                if let Some(raw) = raw {
3110                    __validate_urlencoded(raw)?;
3111                }
3112                let __pairs = __query_pairs(raw);
3113                #(#decoders)*
3114                Ok(#ident {
3115                    #(#field_idents),*
3116                })
3117            }
3118        })
3119    }
3120
3121    fn emit_errors(
3122        &self,
3123        ops: &[&OperationInfo],
3124        validation_enabled: bool,
3125        response_enum_names: &BTreeMap<String, syn::Ident>,
3126    ) -> TokenStream {
3127        let provenance_attribute = self.provenance_attribute();
3128        let any_streaming = ops.iter().any(|op| op.supports_streaming);
3129        let enums: Vec<TokenStream> = ops
3130            .iter()
3131            .map(|op| self.emit_response_enum(op, response_enum_names))
3132            .collect();
3133        let problem_types = validation_enabled.then(|| {
3134            quote! {
3135                /// RFC 9457 Problem Details profile used for rejected requests.
3136                #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)]
3137                pub struct ProblemDetails {
3138                    #[serde(rename = "type")]
3139                    pub r#type: String,
3140                    pub title: String,
3141                    pub status: u16,
3142                    pub code: String,
3143                    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3144                    pub errors: Vec<InvalidParameter>,
3145                }
3146
3147                /// One sanitized request-contract violation.
3148                #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)]
3149                pub struct InvalidParameter {
3150                    pub code: String,
3151                    pub location: String,
3152                    pub message: String,
3153                }
3154
3155                /// Axum rejection wrapper which always uses `application/problem+json`.
3156                #[derive(Debug, Clone)]
3157                pub struct RequestValidationRejection(pub ProblemDetails);
3158
3159                impl IntoResponse for RequestValidationRejection {
3160                    fn into_response(self) -> ::axum::response::Response {
3161                        let status = StatusCode::from_u16(self.0.status)
3162                            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
3163                        let mut response = (status, Json(self.0)).into_response();
3164                        response.headers_mut().insert(
3165                            ::axum::http::header::CONTENT_TYPE,
3166                            ::axum::http::HeaderValue::from_static("application/problem+json"),
3167                        );
3168                        response
3169                    }
3170                }
3171            }
3172        });
3173
3174        // The SSE type alias is emitted exactly when at least one
3175        // picked op streams. Bringing it in unconditionally would force
3176        // `futures-core` into the user's dep tree even when they don't
3177        // need it.
3178        let stream_alias = if any_streaming {
3179            quote! {
3180                /// Stream payload carried by `*Stream` variants. Each
3181                /// yielded item is a pre-built `axum::response::sse::Event`.
3182                pub type ServerEventStream = ::std::pin::Pin<
3183                    Box<
3184                        dyn ::futures_core::Stream<
3185                                Item = ::std::result::Result<
3186                                    ::axum::response::sse::Event,
3187                                    ::std::convert::Infallible,
3188                                >,
3189                            > + ::std::marker::Send
3190                            + 'static,
3191                    >,
3192                >;
3193
3194                /// Wrap any `Stream<Item = Result<Event, Infallible>>` in
3195                /// a `Sse<ServerEventStream>` ready to drop into the
3196                /// `OkStream` variant. Replaces the
3197                /// `Sse::new(Box::pin(...))` dance.
3198                pub fn sse_response<S>(stream: S) -> ::axum::response::sse::Sse<ServerEventStream>
3199                where
3200                    S: ::futures_core::Stream<
3201                            Item = ::std::result::Result<
3202                                ::axum::response::sse::Event,
3203                                ::std::convert::Infallible,
3204                            >,
3205                        > + ::std::marker::Send
3206                        + 'static,
3207                {
3208                    ::axum::response::sse::Sse::new(Box::pin(stream))
3209                }
3210            }
3211        } else {
3212            quote! {}
3213        };
3214
3215        // We deliberately do NOT import `axum::response::Response` here:
3216        // many specs declare a schema literally named `Response`
3217        // (OpenAI's `createResponse` is one such case), and an explicit
3218        // import would shadow the glob-imported schema name. The
3219        // IntoResponse impl returns `axum::response::Response`
3220        // fully qualified.
3221        quote! {
3222            //! Per-operation response enums. Pick a variant to pick a
3223            //! status code — IntoResponse maps each variant to its
3224            //! documented (StatusCode, Json) pair.
3225
3226            #provenance_attribute
3227
3228            #![allow(clippy::large_enum_variant)]
3229
3230            use axum::{
3231                http::StatusCode,
3232                response::IntoResponse,
3233                Json,
3234            };
3235            // Schemas live in `<parent>/types.rs`. Reaching them via
3236            // `super::super::types::*` instead of a glob on the
3237            // parent module keeps these imports stable regardless of
3238            // how the user mounts the generated tree.
3239            #[allow(unused_imports)]
3240            use super::super::types::*;
3241
3242            #problem_types
3243
3244            #stream_alias
3245
3246            #(#enums)*
3247        }
3248    }
3249
3250    fn emit_response_enum(
3251        &self,
3252        op: &OperationInfo,
3253        response_enum_names: &BTreeMap<String, syn::Ident>,
3254    ) -> TokenStream {
3255        let enum_ident = &response_enum_names[&op.operation_id];
3256        let mut variants: Vec<TokenStream> = Vec::new();
3257        let mut arms: Vec<TokenStream> = Vec::new();
3258
3259        // Analyses produced before complete response metadata existed (and a
3260        // few unit tests that construct OperationInfo directly) still expose
3261        // `response_schemas`. Keep that compatibility path while treating the
3262        // complete response map as authoritative for real specs.
3263        let fallback_responses;
3264        let responses = if let Some(responses) = self
3265            .analysis
3266            .operation_responses
3267            .get(&op.operation_id)
3268            .filter(|responses| !responses.is_empty())
3269        {
3270            responses
3271        } else {
3272            fallback_responses = op
3273                .response_schemas
3274                .iter()
3275                .map(|(status, schema_name)| {
3276                    (
3277                        status.clone(),
3278                        OperationResponse {
3279                            schema_name: Some(schema_name.clone()),
3280                            media_type: Some("application/json".to_string()),
3281                            body: Some(OperationResponseBody::Json {
3282                                schema_name: schema_name.clone(),
3283                                media_type: "application/json".to_string(),
3284                            }),
3285                            supports_streaming: false,
3286                            has_content: true,
3287                            unsupported_media_types: Vec::new(),
3288                        },
3289                    )
3290                })
3291                .collect::<BTreeMap<_, _>>();
3292            &fallback_responses
3293        };
3294
3295        for (status, response) in responses {
3296            let base_name = status_variant_name(status);
3297            let variant = format_ident!("{}", base_name);
3298            let runtime_status = response_uses_runtime_status(status);
3299            let status_expr = status_token(status);
3300            let status_guard = runtime_status_guard(status, quote! { status });
3301
3302            let buffered_body = response.body.clone().or_else(|| {
3303                response
3304                    .schema_name
3305                    .as_ref()
3306                    .map(|schema_name| OperationResponseBody::Json {
3307                        schema_name: schema_name.clone(),
3308                        media_type: response
3309                            .media_type
3310                            .clone()
3311                            .unwrap_or_else(|| "application/json".to_string()),
3312                    })
3313            });
3314
3315            if let Some(body) = buffered_body {
3316                let (body_ty, response_body, media_type, wildcard) = match body {
3317                    OperationResponseBody::Json {
3318                        schema_name,
3319                        media_type,
3320                    } => (
3321                        self.model_type(&schema_name),
3322                        quote! { Json(body) },
3323                        media_type,
3324                        false,
3325                    ),
3326                    OperationResponseBody::Text { media_type } => {
3327                        (quote! { String }, quote! { body }, media_type, false)
3328                    }
3329                    OperationResponseBody::Binary {
3330                        media_type,
3331                        wildcard,
3332                    } => (
3333                        quote! { bytes::Bytes },
3334                        quote! { body },
3335                        media_type,
3336                        wildcard,
3337                    ),
3338                };
3339                if wildcard {
3340                    if runtime_status {
3341                        variants.push(quote! {
3342                            #variant(StatusCode, ::axum::http::HeaderValue, #body_ty)
3343                        });
3344                        arms.push(quote! {
3345                            Self::#variant(status, content_type, body) => {
3346                                if !(#status_guard) {
3347                                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3348                                }
3349                                let mut response = (status, #response_body).into_response();
3350                                response.headers_mut().insert(
3351                                    ::axum::http::header::CONTENT_TYPE,
3352                                    content_type,
3353                                );
3354                                response
3355                            }
3356                        });
3357                    } else {
3358                        variants.push(quote! {
3359                            #variant(::axum::http::HeaderValue, #body_ty)
3360                        });
3361                        arms.push(quote! {
3362                            Self::#variant(content_type, body) => {
3363                                let mut response = (#status_expr, #response_body).into_response();
3364                                response.headers_mut().insert(
3365                                    ::axum::http::header::CONTENT_TYPE,
3366                                    content_type,
3367                                );
3368                                response
3369                            }
3370                        });
3371                    }
3372                } else if runtime_status {
3373                    variants.push(quote! { #variant(StatusCode, #body_ty) });
3374                    arms.push(quote! {
3375                        Self::#variant(status, body) => {
3376                            if !(#status_guard) {
3377                                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3378                            }
3379                            let mut response = (status, #response_body).into_response();
3380                            let Ok(content_type) = ::axum::http::HeaderValue::from_bytes(#media_type.as_bytes()) else {
3381                                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3382                            };
3383                            response.headers_mut().insert(
3384                                ::axum::http::header::CONTENT_TYPE,
3385                                content_type,
3386                            );
3387                            response
3388                        }
3389                    });
3390                } else {
3391                    variants.push(quote! { #variant(#body_ty) });
3392                    arms.push(quote! {
3393                        Self::#variant(body) => {
3394                            let mut response = (#status_expr, #response_body).into_response();
3395                            let Ok(content_type) = ::axum::http::HeaderValue::from_bytes(#media_type.as_bytes()) else {
3396                                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3397                            };
3398                            response.headers_mut().insert(
3399                                ::axum::http::header::CONTENT_TYPE,
3400                                content_type,
3401                            );
3402                            response
3403                        }
3404                    });
3405                }
3406            } else if !response.has_content {
3407                if runtime_status {
3408                    variants.push(quote! { #variant(StatusCode) });
3409                    arms.push(quote! {
3410                        Self::#variant(status) => {
3411                            if #status_guard {
3412                                status.into_response()
3413                            } else {
3414                                StatusCode::INTERNAL_SERVER_ERROR.into_response()
3415                            }
3416                        }
3417                    });
3418                } else {
3419                    variants.push(quote! { #variant });
3420                    arms.push(quote! {
3421                        Self::#variant => #status_expr.into_response()
3422                    });
3423                }
3424            }
3425
3426            // The stream variant belongs to the response which declared SSE,
3427            // so it carries that response's status instead of implicitly 200.
3428            if response.supports_streaming {
3429                let stream_variant = format_ident!("{}Stream", base_name);
3430                if runtime_status {
3431                    variants.push(quote! {
3432                        #stream_variant(StatusCode, ::axum::response::sse::Sse<ServerEventStream>)
3433                    });
3434                    arms.push(quote! {
3435                        Self::#stream_variant(status, sse) => {
3436                            if !(#status_guard) {
3437                                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3438                            }
3439                            let mut response = sse.into_response();
3440                            *response.status_mut() = status;
3441                            response
3442                        }
3443                    });
3444                } else {
3445                    variants.push(quote! {
3446                        #stream_variant(::axum::response::sse::Sse<ServerEventStream>)
3447                    });
3448                    arms.push(quote! {
3449                        Self::#stream_variant(sse) => {
3450                            let mut response = sse.into_response();
3451                            *response.status_mut() = #status_expr;
3452                            response
3453                        }
3454                    });
3455                }
3456            }
3457        }
3458
3459        // Fallback: if no response variants were declared we still need
3460        // a no-op enum so the trait method has a return type. Use an
3461        // empty `Empty` variant returning 204.
3462        if variants.is_empty() {
3463            variants.push(quote! { Empty });
3464            arms.push(quote! {
3465                Self::Empty => StatusCode::NO_CONTENT.into_response()
3466            });
3467        }
3468
3469        let doc = format!(
3470            " Response for `{} {}` (operationId `{}`).",
3471            op.method, op.path, op.operation_id
3472        );
3473
3474        quote! {
3475            #[doc = #doc]
3476            pub enum #enum_ident {
3477                #(#variants),*
3478            }
3479
3480            impl IntoResponse for #enum_ident {
3481                fn into_response(self) -> ::axum::response::Response {
3482                    match self {
3483                        #(#arms),*
3484                    }
3485                }
3486            }
3487        }
3488    }
3489}
3490
3491fn parameter_location(location: &str, name: &str) -> String {
3492    format!("/{location}/{}", name.replace('~', "~0").replace('/', "~1"))
3493}
3494
3495/// Emit a string-enum type for a parameter whose inline schema
3496/// declared `enum: [...]`. The analyzer sets `rust_type` to a
3497/// synthetic name (`{OpId}{Param}` in PascalCase) and surfaces the
3498/// values; the codegen layer is what actually writes the enum.
3499fn emit_param_enum(name: &str, values: &[String]) -> TokenStream {
3500    let enum_ident = format_ident!("{}", name);
3501    let variants: Vec<TokenStream> = values
3502        .iter()
3503        .enumerate()
3504        .map(|(i, raw)| {
3505            let pascal = raw.to_pascal_case();
3506            // PascalCase can produce an empty string (pure-symbol
3507            // input) or an identifier starting with a digit
3508            // (e.g. `1d` stays `1d`) — both invalid as Rust idents.
3509            // Fall back to a positional name so the enum compiles.
3510            let starts_with_digit = pascal
3511                .chars()
3512                .next()
3513                .map(|c| c.is_ascii_digit())
3514                .unwrap_or(true);
3515            let v_name = if pascal.is_empty() || starts_with_digit {
3516                format!("Variant{i}")
3517            } else {
3518                pascal
3519            };
3520            let v_ident = format_ident!("{}", v_name);
3521            let default_marker = if i == 0 {
3522                quote! { #[default] }
3523            } else {
3524                quote! {}
3525            };
3526            quote! {
3527                #default_marker
3528                #[serde(rename = #raw)]
3529                #v_ident
3530            }
3531        })
3532        .collect();
3533    quote! {
3534        #[derive(Debug, Clone, PartialEq, Eq, ::serde::Deserialize, ::serde::Serialize, Default)]
3535        pub enum #enum_ident {
3536            #(#variants),*
3537        }
3538    }
3539}
3540
3541fn axum_method_call(method: &str) -> Option<TokenStream> {
3542    match method.to_ascii_uppercase().as_str() {
3543        "CONNECT" => Some(quote! { connect }),
3544        "DELETE" => Some(quote! { delete }),
3545        "GET" => Some(quote! { get }),
3546        "HEAD" => Some(quote! { head }),
3547        "OPTIONS" => Some(quote! { options }),
3548        "PATCH" => Some(quote! { patch }),
3549        "POST" => Some(quote! { post }),
3550        "PUT" => Some(quote! { put }),
3551        "TRACE" => Some(quote! { trace }),
3552        _ => None,
3553    }
3554}
3555
3556/// Build one exact-method dispatcher for every nonstandard operation sharing a
3557/// path within one generated trait. Axum has convenience functions for the
3558/// standard RFC methods, but OpenAPI 3.2 also defines QUERY and permits custom
3559/// `additionalOperations`. Axum allows only one `any` fallback per path, so all
3560/// custom methods on that path and trait must share this dispatcher. Generation
3561/// rejects the cross-trait form before reaching this helper.
3562fn axum_custom_route(
3563    path: &str,
3564    dispatcher: &syn::Ident,
3565    methods: &[(String, syn::Ident)],
3566    trait_ident: &syn::Ident,
3567) -> (TokenStream, TokenStream) {
3568    let arms = methods.iter().map(|(method, handler)| {
3569        quote! {
3570            #method => ::axum::handler::Handler::call(#handler::<T>, request, api).await
3571        }
3572    });
3573    let route = quote! {
3574        .route(#path, ::axum::routing::any(#dispatcher::<T>))
3575    };
3576    let dispatcher_fn = quote! {
3577        async fn #dispatcher<T>(
3578            ::axum::extract::State(api): ::axum::extract::State<T>,
3579            request: ::axum::extract::Request,
3580        ) -> ::axum::response::Response
3581        where
3582            T: #trait_ident + Clone + Send + Sync + 'static,
3583        {
3584            match request.method().as_str() {
3585                #(#arms,)*
3586                _ => ::axum::response::IntoResponse::into_response(
3587                    ::axum::http::StatusCode::METHOD_NOT_ALLOWED,
3588                ),
3589            }
3590        }
3591    };
3592    (route, dispatcher_fn)
3593}
3594
3595fn path_parameter_affixes<'a>(path: &'a str, parameter_name: &str) -> Option<(&'a str, &'a str)> {
3596    let marker = format!("{{{parameter_name}}}");
3597    for segment in path.split('/') {
3598        let Some(start) = segment.find(&marker) else {
3599            continue;
3600        };
3601        let prefix = &segment[..start];
3602        let suffix = &segment[start + marker.len()..];
3603        if prefix.is_empty() && suffix.is_empty() {
3604            return None;
3605        }
3606        return Some((prefix, suffix));
3607    }
3608    None
3609}
3610
3611/// Validate and convert an OpenAPI path template into Axum 0.8 route syntax.
3612///
3613/// Both formats use `{parameter}` for a dynamic segment. Axum only supports a
3614/// capture as a complete segment, so OpenAPI templates embedded in a literal
3615/// segment are rejected during generation instead of panicking when the
3616/// generated router is constructed.
3617fn openapi_to_axum_path(path: &str) -> Result<String, ServerCodegenError> {
3618    let invalid = |reason: &str| ServerCodegenError::InvalidRoutePath {
3619        path: path.to_string(),
3620        reason: reason.to_string(),
3621    };
3622
3623    if !path.starts_with('/') {
3624        return Err(invalid("paths must start with `/`"));
3625    }
3626    let mut route_segments = Vec::new();
3627    for segment in path.split('/').skip(1) {
3628        if segment.is_empty() {
3629            route_segments.push(String::new());
3630            continue;
3631        }
3632        if segment.starts_with(':') || segment.starts_with('*') {
3633            return Err(invalid(
3634                "segments beginning with `:` or `*` conflict with Axum route syntax",
3635            ));
3636        }
3637
3638        let has_open = segment.contains('{');
3639        let has_close = segment.contains('}');
3640        if has_open || has_close {
3641            let Some(open) = segment.find('{') else {
3642                return Err(invalid(
3643                    "path parameter has a closing brace without an opening brace",
3644                ));
3645            };
3646            let Some(relative_close) = segment[open + 1..].find('}') else {
3647                return Err(invalid(
3648                    "path parameter has an opening brace without a closing brace",
3649                ));
3650            };
3651            let close = open + 1 + relative_close;
3652            let name = &segment[open + 1..close];
3653            let prefix = &segment[..open];
3654            let suffix = &segment[close + 1..];
3655            if name.is_empty() || name.contains(['{', '}']) {
3656                return Err(invalid(
3657                    "path parameter names must be non-empty and cannot contain braces",
3658                ));
3659            }
3660            if prefix.contains(['{', '}']) || suffix.contains(['{', '}']) {
3661                return Err(invalid(
3662                    "embedded path segments may contain exactly one parameter",
3663                ));
3664            }
3665            route_segments.push(format!("{{{name}}}"));
3666        } else {
3667            route_segments.push(segment.to_string());
3668        }
3669    }
3670
3671    Ok(format!("/{}", route_segments.join("/")))
3672}
3673
3674fn body_type(op: &OperationInfo) -> Option<String> {
3675    match &op.request_body {
3676        Some(RequestBodyContent::Json { schema_name, .. })
3677        | Some(RequestBodyContent::FormUrlEncoded { schema_name, .. })
3678        | Some(RequestBodyContent::Multipart { schema_name, .. }) => Some(schema_name.clone()),
3679        Some(RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. }) => {
3680            Some("bytes::Bytes".to_string())
3681        }
3682        Some(RequestBodyContent::TextPlain { .. }) => Some("String".to_string()),
3683        _ => None,
3684    }
3685}
3686
3687fn group_by_tag<'a>(ops: &[&'a OperationInfo]) -> BTreeMap<String, Vec<&'a OperationInfo>> {
3688    let mut groups: BTreeMap<String, Vec<&OperationInfo>> = BTreeMap::new();
3689    for op in ops {
3690        let tag = primary_tag(op);
3691        groups.entry(tag).or_default().push(op);
3692    }
3693    groups
3694}
3695
3696fn primary_tag(op: &OperationInfo) -> String {
3697    op.tags.first().cloned().unwrap_or_else(|| "Server".into())
3698}
3699
3700/// Reject distinct raw primary tags which would emit the same Rust items.
3701/// Sorting the raw tags first keeps the selected pair and diagnostic stable
3702/// even when selectors are reordered in configuration.
3703fn validate_tag_identifier_collisions(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
3704    let raw_tags: std::collections::BTreeSet<String> =
3705        ops.iter().map(|operation| primary_tag(operation)).collect();
3706    let mut raw_by_identifier: BTreeMap<String, String> = BTreeMap::new();
3707    for raw_tag in raw_tags {
3708        let identifier = trait_ident_for_tag(&raw_tag).to_string();
3709        if let Some(first_tag) = raw_by_identifier.insert(identifier.clone(), raw_tag.clone()) {
3710            return Err(ServerCodegenError::TagIdentifierCollision {
3711                first_tag,
3712                second_tag: raw_tag,
3713                identifier,
3714            });
3715        }
3716    }
3717    Ok(())
3718}
3719
3720fn validate_custom_method_route_groups(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
3721    let mut tags_by_path: BTreeMap<&str, std::collections::BTreeSet<String>> = BTreeMap::new();
3722    for op in ops {
3723        if axum_method_call(&op.method).is_none() {
3724            tags_by_path
3725                .entry(&op.path)
3726                .or_default()
3727                .insert(primary_tag(op));
3728        }
3729    }
3730    if let Some((path, tags)) = tags_by_path.into_iter().find(|(_, tags)| tags.len() > 1) {
3731        return Err(ServerCodegenError::CrossTagCustomMethods {
3732            path: path.to_string(),
3733            tags: tags.into_iter().collect::<Vec<_>>().join(", "),
3734        });
3735    }
3736    Ok(())
3737}
3738
3739fn canonical_axum_route_shape(path: &str) -> String {
3740    path.split('/')
3741        .map(|segment| {
3742            if segment.starts_with('{') && segment.ends_with('}') {
3743                "{}"
3744            } else {
3745                segment
3746            }
3747        })
3748        .collect::<Vec<_>>()
3749        .join("/")
3750}
3751
3752fn validate_normalized_route_collisions(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
3753    let mut seen: BTreeMap<(String, String), &str> = BTreeMap::new();
3754    for op in ops {
3755        let normalized = openapi_to_axum_path(&op.path)?;
3756        let key = (
3757            canonical_axum_route_shape(&normalized),
3758            op.method.to_ascii_uppercase(),
3759        );
3760        if let Some(previous) = seen.insert(key, &op.path)
3761            && previous != op.path
3762        {
3763            return Err(ServerCodegenError::InvalidRoutePath {
3764                path: op.path.clone(),
3765                reason: format!(
3766                    "normalizes to the same Axum route and method as `{previous}`; embedded path affixes must remain unambiguous"
3767                ),
3768            });
3769        }
3770    }
3771    Ok(())
3772}
3773
3774fn trait_ident_for_tag(tag: &str) -> syn::Ident {
3775    let pascal = tag.to_pascal_case();
3776    let base = if pascal.is_empty() {
3777        "Server".into()
3778    } else {
3779        pascal
3780    };
3781    format_ident!("{}Api", base)
3782}
3783
3784/// Convert a status code (or `default`, or wildcard `4XX`) to a
3785/// variant identifier.
3786/// Rust response-enum variant name for an OpenAPI response key.
3787pub fn status_variant_name(status: &str) -> String {
3788    match status {
3789        "200" => "Ok".into(),
3790        "201" => "Created".into(),
3791        "202" => "Accepted".into(),
3792        "204" => "NoContent".into(),
3793        "301" => "MovedPermanently".into(),
3794        "302" => "Found".into(),
3795        "304" => "NotModified".into(),
3796        "400" => "BadRequest".into(),
3797        "401" => "Unauthorized".into(),
3798        "403" => "Forbidden".into(),
3799        "404" => "NotFound".into(),
3800        "409" => "Conflict".into(),
3801        "410" => "Gone".into(),
3802        "422" => "UnprocessableEntity".into(),
3803        "429" => "TooManyRequests".into(),
3804        "500" => "InternalServerError".into(),
3805        "502" => "BadGateway".into(),
3806        "503" => "ServiceUnavailable".into(),
3807        "default" => "Default".into(),
3808        "1XX" => "Informational".into(),
3809        "2XX" => "Success".into(),
3810        "3XX" => "Redirection".into(),
3811        "4XX" => "ClientError".into(),
3812        "5XX" => "ServerError".into(),
3813        other => format!("Status{}", other.to_ascii_uppercase().replace('X', "x")),
3814    }
3815}
3816
3817fn response_uses_runtime_status(status: &str) -> bool {
3818    status == "default" || matches!(status.as_bytes(), [b'1'..=b'5', b'X' | b'x', b'X' | b'x'])
3819}
3820
3821fn runtime_status_guard(status: &str, value: TokenStream) -> TokenStream {
3822    if status == "default" {
3823        quote! { true }
3824    } else {
3825        let class = u16::from(
3826            status
3827                .as_bytes()
3828                .first()
3829                .map(|digit| digit - b'0')
3830                .unwrap_or_default(),
3831        );
3832        quote! { #value.as_u16() / 100 == #class }
3833    }
3834}
3835
3836/// Emit a StatusCode expression for a status string. Numeric codes use
3837/// the named constants where possible; wildcard ranges and `default`
3838/// pick a representative code (the lowest in-range).
3839fn status_token(status: &str) -> TokenStream {
3840    match status {
3841        "200" => quote! { StatusCode::OK },
3842        "201" => quote! { StatusCode::CREATED },
3843        "202" => quote! { StatusCode::ACCEPTED },
3844        "204" => quote! { StatusCode::NO_CONTENT },
3845        "301" => quote! { StatusCode::MOVED_PERMANENTLY },
3846        "302" => quote! { StatusCode::FOUND },
3847        "304" => quote! { StatusCode::NOT_MODIFIED },
3848        "400" => quote! { StatusCode::BAD_REQUEST },
3849        "401" => quote! { StatusCode::UNAUTHORIZED },
3850        "403" => quote! { StatusCode::FORBIDDEN },
3851        "404" => quote! { StatusCode::NOT_FOUND },
3852        "409" => quote! { StatusCode::CONFLICT },
3853        "410" => quote! { StatusCode::GONE },
3854        "422" => quote! { StatusCode::UNPROCESSABLE_ENTITY },
3855        "429" => quote! { StatusCode::TOO_MANY_REQUESTS },
3856        "500" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
3857        "502" => quote! { StatusCode::BAD_GATEWAY },
3858        "503" => quote! { StatusCode::SERVICE_UNAVAILABLE },
3859        "default" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
3860        // Range/default responses carry a runtime StatusCode in their enum
3861        // variant and never reach this fixed-status helper.
3862        "1XX" | "2XX" | "3XX" | "4XX" | "5XX" => {
3863            quote! { StatusCode::INTERNAL_SERVER_ERROR }
3864        }
3865        // Specific numeric codes not in our table — fall back to
3866        // StatusCode::from_u16. Codegen ensures a panic-free path by
3867        // unwrapping on a value that must parse (we already
3868        // know the spec wrote a numeric status here).
3869        other => {
3870            if let Ok(n) = other.parse::<u16>() {
3871                quote! {
3872                    StatusCode::from_u16(#n).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
3873                }
3874            } else {
3875                quote! { StatusCode::INTERNAL_SERVER_ERROR }
3876            }
3877        }
3878    }
3879}
3880
3881fn parse_type(ty: &str) -> TokenStream {
3882    syn::parse_str::<syn::Type>(ty)
3883        .map(|t| quote! { #t })
3884        .unwrap_or_else(|_| {
3885            let ident = format_ident!("{}", ty);
3886            quote! { #ident }
3887        })
3888}
3889
3890fn format_or_raw(ts: TokenStream) -> String {
3891    let raw = ts.to_string();
3892    match syn::parse_file(&raw) {
3893        Ok(parsed) => prettyplease::unparse(&parsed),
3894        Err(_) => raw,
3895    }
3896}
3897
3898#[cfg(test)]
3899mod tests {
3900    use super::*;
3901
3902    #[test]
3903    fn status_variant_name_maps_known_codes() {
3904        assert_eq!(status_variant_name("200"), "Ok");
3905        assert_eq!(status_variant_name("4XX"), "ClientError");
3906        assert_eq!(status_variant_name("default"), "Default");
3907        assert_eq!(status_variant_name("418"), "Status418");
3908    }
3909
3910    #[test]
3911    fn trait_ident_for_tag_appends_api() {
3912        let id = trait_ident_for_tag("Responses");
3913        assert_eq!(id.to_string(), "ResponsesApi");
3914    }
3915
3916    #[test]
3917    fn embedded_path_affix_route_collisions_are_rejected() {
3918        let json = OperationInfo {
3919            operation_id: "json".into(),
3920            method: "get".into(),
3921            path: "/specs/{id}.json".into(),
3922            ..Default::default()
3923        };
3924        let yaml = OperationInfo {
3925            operation_id: "yaml".into(),
3926            method: "get".into(),
3927            path: "/specs/{name}.yaml".into(),
3928            ..Default::default()
3929        };
3930        let error = validate_normalized_route_collisions(&[&json, &yaml]).unwrap_err();
3931        assert!(error.to_string().contains("same Axum route"), "{error}");
3932    }
3933
3934    #[test]
3935    fn untagged_falls_back_to_server_api() {
3936        let id = trait_ident_for_tag("");
3937        assert_eq!(id.to_string(), "ServerApi");
3938    }
3939
3940    #[test]
3941    fn colliding_raw_tags_are_rejected_in_stable_order() {
3942        let first = OperationInfo {
3943            operation_id: "first".into(),
3944            tags: vec!["foo_bar".into()],
3945            ..Default::default()
3946        };
3947        let second = OperationInfo {
3948            operation_id: "second".into(),
3949            tags: vec!["foo-bar".into()],
3950            ..Default::default()
3951        };
3952        let error = validate_tag_identifier_collisions(&[&first, &second]).unwrap_err();
3953        assert!(matches!(
3954            error,
3955            ServerCodegenError::TagIdentifierCollision {
3956                first_tag,
3957                second_tag,
3958                identifier,
3959            } if first_tag == "foo-bar"
3960                && second_tag == "foo_bar"
3961                && identifier == "FooBarApi"
3962        ));
3963    }
3964
3965    #[test]
3966    fn custom_methods_on_one_path_share_an_exact_dispatcher() {
3967        let dispatcher = format_ident!("cache_custom_method_dispatch");
3968        let methods = vec![
3969            ("PURGE".to_string(), format_ident!("purge_cache_handler")),
3970            ("QUERY".to_string(), format_ident!("query_cache_handler")),
3971        ];
3972        let trait_ident = format_ident!("CacheApi");
3973        let (route, dispatcher) = axum_custom_route("/cache", &dispatcher, &methods, &trait_ident);
3974        let route = route.to_string();
3975        let dispatcher = dispatcher.to_string();
3976        assert!(route.contains("routing :: any"));
3977        assert_eq!(route.matches("routing :: any").count(), 1);
3978        assert!(dispatcher.contains("\"PURGE\""));
3979        assert!(dispatcher.contains("\"QUERY\""));
3980        assert!(dispatcher.contains("purge_cache_handler"));
3981        assert!(dispatcher.contains("query_cache_handler"));
3982        assert!(dispatcher.contains("METHOD_NOT_ALLOWED"));
3983    }
3984
3985    #[test]
3986    fn standard_methods_use_axum_method_routes_without_guards() {
3987        assert!(axum_method_call("TRACE").is_some());
3988        assert!(axum_method_call("QUERY").is_none());
3989    }
3990
3991    #[test]
3992    fn openapi_parameterized_paths_are_axum_08_paths() {
3993        assert_eq!(
3994            openapi_to_axum_path("/pets/{pet_id}").unwrap(),
3995            "/pets/{pet_id}"
3996        );
3997        assert_eq!(openapi_to_axum_path("/").unwrap(), "/");
3998        assert_eq!(
3999            openapi_to_axum_path("/specs/{provider}/{api}.json").unwrap(),
4000            "/specs/{provider}/{api}"
4001        );
4002    }
4003
4004    #[test]
4005    fn embedded_path_parameter_affixes_are_preserved_for_extraction() {
4006        assert_eq!(
4007            path_parameter_affixes("/specs/{provider}/{api}.json", "api"),
4008            Some(("", ".json"))
4009        );
4010        assert_eq!(
4011            path_parameter_affixes("/{provider}.json", "provider"),
4012            Some(("", ".json"))
4013        );
4014        assert_eq!(path_parameter_affixes("/pets/{id}", "id"), None);
4015    }
4016
4017    #[test]
4018    fn malformed_or_unsupported_route_templates_are_rejected() {
4019        for path in [
4020            "pets/{pet_id}",
4021            "/pets/{first}-{second}",
4022            "/pets/{}",
4023            "/pets/:id",
4024        ] {
4025            assert!(
4026                matches!(
4027                    openapi_to_axum_path(path),
4028                    Err(ServerCodegenError::InvalidRoutePath { .. })
4029                ),
4030                "{path} should be rejected"
4031            );
4032        }
4033    }
4034}