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