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