Skip to main content

openapi_to_rust/server/
codegen.rs

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