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