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, ParameterInfo, QuerySerialization,
12    RequestBodyContent, SchemaAnalysis, SchemaType,
13};
14use crate::config::ServerSection;
15use crate::generator::{CodeGenerator, GeneratedFile, GeneratorConfig};
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/// Compute the set of schema names transitively reachable from the
25/// request/response/parameter shapes of the given operations.
26///
27/// Used by client/server model pruning to drop unreferenced types from
28/// `types.rs`. Walks every `$ref` in each schema's raw JSON
29/// (`AnalyzedSchema.original`) rather than the analyzer's
30/// `dependencies` field — the latter is incomplete for some
31/// schemas (e.g. struct fields whose target schemas weren't
32/// individually tracked).
33///
34/// Inline parameter enums (whose `rust_type` is a synthetic name
35/// without a matching `analysis.schemas` entry) are not the
36/// responsibility of this walk — they're emitted directly by the
37/// server codegen from `parameter.enum_values`.
38pub fn reachable_schemas(
39    analysis: &SchemaAnalysis,
40    ops: &[&OperationInfo],
41) -> std::collections::BTreeSet<String> {
42    reachable_schemas_with_roots(analysis, ops, &[])
43}
44
45/// [`reachable_schemas`] plus explicit schema roots used by configured
46/// consumers such as SSE event-union types.
47pub fn reachable_schemas_with_roots(
48    analysis: &SchemaAnalysis,
49    ops: &[&OperationInfo],
50    extra_roots: &[String],
51) -> std::collections::BTreeSet<String> {
52    let mut keep: std::collections::BTreeSet<String> = Default::default();
53    let mut queue: Vec<String> = Vec::new();
54
55    let seed =
56        |name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
57            if !name.is_empty() && keep.insert(name.to_string()) {
58                queue.push(name.to_string());
59            }
60        };
61
62    for op in ops {
63        if let Some(rb) = &op.request_body
64            && let Some(name) = rb.schema_name()
65        {
66            seed(name, &mut queue, &mut keep);
67        }
68        for ty in op.response_schemas.values() {
69            seed(ty, &mut queue, &mut keep);
70        }
71        for p in &op.parameters {
72            if let Some(name) = &p.schema_ref {
73                seed(name, &mut queue, &mut keep);
74            }
75            if let Some(
76                QuerySerialization::FormExplodedArray {
77                    item_type: crate::analysis::ArrayItemType::EnumRef(name),
78                }
79                | QuerySerialization::FormArray {
80                    item_type: crate::analysis::ArrayItemType::EnumRef(name),
81                },
82            ) = &p.query_serialization
83            {
84                seed(name, &mut queue, &mut keep);
85            }
86        }
87    }
88    for root in extra_roots {
89        seed(root, &mut queue, &mut keep);
90    }
91
92    while let Some(name) = queue.pop() {
93        if let Some(schema) = analysis.schemas.get(&name) {
94            // Walk the raw JSON for every `$ref` string and feed
95            // the referenced schema names back into the queue.
96            collect_refs(&schema.original, &mut queue, &mut keep);
97            // Belt-and-braces: also include the analyzer's tracked
98            // dependencies, which sometimes catch refs that live
99            // outside the immediate JSON tree (e.g. allOf compositions
100            // resolved before the snapshot was captured).
101            for dep in &schema.dependencies {
102                seed(dep, &mut queue, &mut keep);
103            }
104            // The analyzed shape is the authoritative generated type graph.
105            // It includes ownership edges for inline/synthetic schemas that
106            // do not appear as `$ref`s in the source document.
107            collect_schema_type_refs(&schema.schema_type, &mut queue, &mut keep);
108        }
109    }
110
111    keep
112}
113
114fn collect_schema_type_refs(
115    schema_type: &SchemaType,
116    queue: &mut Vec<String>,
117    keep: &mut std::collections::BTreeSet<String>,
118) {
119    let seed =
120        |name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
121            if !name.is_empty() && keep.insert(name.to_string()) {
122                queue.push(name.to_string());
123            }
124        };
125
126    match schema_type {
127        SchemaType::Primitive { .. }
128        | SchemaType::StringEnum { .. }
129        | SchemaType::ExtensibleEnum { .. } => {}
130        SchemaType::Object {
131            properties,
132            additional_properties,
133            ..
134        } => {
135            for property in properties.values() {
136                collect_schema_type_refs(&property.schema_type, queue, keep);
137            }
138            if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
139                collect_schema_type_refs(value_type, queue, keep);
140            }
141        }
142        SchemaType::DiscriminatedUnion { variants, .. } => {
143            for variant in variants {
144                seed(&variant.type_name, queue, keep);
145            }
146        }
147        SchemaType::Union { variants } | SchemaType::Composition { schemas: variants } => {
148            for variant in variants {
149                seed(&variant.target, queue, keep);
150            }
151        }
152        SchemaType::Array { item_type } => collect_schema_type_refs(item_type, queue, keep),
153        SchemaType::Reference { target } => seed(target, queue, keep),
154    }
155}
156
157fn collect_refs(
158    value: &serde_json::Value,
159    queue: &mut Vec<String>,
160    keep: &mut std::collections::BTreeSet<String>,
161) {
162    match value {
163        serde_json::Value::Object(map) => {
164            for (k, v) in map {
165                if k == "$ref"
166                    && let Some(s) = v.as_str()
167                    && let Some(name) = s.strip_prefix("#/components/schemas/")
168                    && keep.insert(name.to_string())
169                {
170                    queue.push(name.to_string());
171                }
172                collect_refs(v, queue, keep);
173            }
174        }
175        serde_json::Value::Array(items) => {
176            for v in items {
177                collect_refs(v, queue, keep);
178            }
179        }
180        _ => {}
181    }
182}
183
184#[derive(Debug, thiserror::Error)]
185pub enum ServerCodegenError {
186    #[error("server selector: {0}")]
187    Parse(#[from] super::SelectorParseError),
188    #[error("server selector: {0}")]
189    Resolve(#[from] super::SelectorResolveError),
190    #[error("internal: {0}")]
191    Internal(String),
192    #[error(
193        "cannot generate exact Axum routes for custom HTTP methods on `{path}` across multiple primary tags ({tags}); Axum 0.7 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"
194    )]
195    CrossTagCustomMethods { path: String, tags: String },
196    #[error(
197        "cannot generate Axum query extraction for `{operation_id}` parameter `{parameter}`: {reason}"
198    )]
199    UnsupportedQueryParameter {
200        operation_id: String,
201        parameter: String,
202        reason: String,
203    },
204    #[error(
205        "cannot generate unambiguous Axum query extraction for `{operation_id}`: wire key `{wire_key}` is claimed by both `{first_parameter}` and `{second_parameter}`"
206    )]
207    AmbiguousQueryParameter {
208        operation_id: String,
209        wire_key: String,
210        first_parameter: String,
211        second_parameter: String,
212    },
213}
214
215pub struct ServerCodegen<'a> {
216    config: &'a GeneratorConfig,
217    analysis: &'a SchemaAnalysis,
218    server: &'a ServerSection,
219    source_provenance: Option<String>,
220}
221
222impl<'a> ServerCodegen<'a> {
223    pub fn new(
224        config: &'a GeneratorConfig,
225        analysis: &'a SchemaAnalysis,
226        server: &'a ServerSection,
227    ) -> Self {
228        Self {
229            config,
230            analysis,
231            server,
232            source_provenance: None,
233        }
234    }
235
236    /// Attach a sanitized source label to generated server module headers.
237    pub fn with_source_provenance(mut self, source: Option<&str>) -> Self {
238        self.source_provenance = source.map(str::to_string);
239        self
240    }
241
242    fn provenance_attribute(&self) -> TokenStream {
243        self.source_provenance
244            .as_ref()
245            .map(|source| {
246                let provenance = format!(
247                    " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
248                    env!("CARGO_PKG_VERSION")
249                );
250                quote! { #![doc = #provenance] }
251            })
252            .unwrap_or_default()
253    }
254
255    /// Resolve selectors and emit `server/{mod,api,errors}.rs`.
256    pub fn generate(&self) -> Result<Vec<GeneratedFile>, ServerCodegenError> {
257        if self.server.operations.is_empty() {
258            return Ok(Vec::new());
259        }
260
261        let index = OperationIndex::from_analysis(self.analysis);
262        let selectors: Vec<Selector> = self
263            .server
264            .operations
265            .iter()
266            .map(|s| Selector::parse(s))
267            .collect::<Result<_, _>>()?;
268        let resolution = super::resolve(&selectors, &index)?;
269
270        // Look up full OperationInfo for each resolved op (we need
271        // parameters, request body, response schemas — the summary
272        // only has the display surface).
273        let ops: Vec<&OperationInfo> = resolution
274            .operations
275            .iter()
276            .map(|s| {
277                self.analysis
278                    .operations
279                    .get(&s.operation_id)
280                    .ok_or_else(|| {
281                        ServerCodegenError::Internal(format!(
282                            "operation `{}` resolved but missing from analysis",
283                            s.operation_id
284                        ))
285                    })
286            })
287            .collect::<Result<_, _>>()?;
288        validate_custom_method_route_groups(&ops)?;
289        self.validate_query_parameters(&ops)?;
290
291        // Group by primary tag (first tag wins; untagged → "Server").
292        let groups = group_by_tag(&ops);
293
294        let api_rs = self.emit_api(&groups);
295        let errors_rs = self.emit_errors(&ops);
296        let router_rs = self.emit_router(&groups);
297        let mod_rs = self.emit_mod();
298
299        Ok(vec![
300            GeneratedFile {
301                path: PathBuf::from("server").join("mod.rs"),
302                content: format_or_raw(mod_rs),
303            },
304            GeneratedFile {
305                path: PathBuf::from("server").join("api.rs"),
306                content: format_or_raw(api_rs),
307            },
308            GeneratedFile {
309                path: PathBuf::from("server").join("errors.rs"),
310                content: format_or_raw(errors_rs),
311            },
312            GeneratedFile {
313                path: PathBuf::from("server").join("router.rs"),
314                content: format_or_raw(router_rs),
315            },
316        ])
317    }
318
319    fn query_parameter_type(&self, parameter: &ParameterInfo) -> TokenStream {
320        CodeGenerator::new(self.config.clone()).get_param_owned_rust_type(parameter)
321    }
322
323    fn parameter_ident(&self, parameter: &ParameterInfo) -> syn::Ident {
324        let generator = CodeGenerator::new(self.config.clone());
325        CodeGenerator::to_field_ident(&generator.param_ident_str(parameter))
326    }
327
328    fn resolve_query_schema(&self, schema_name: &str) -> Option<&crate::analysis::AnalyzedSchema> {
329        let mut current = schema_name;
330        let mut visited = std::collections::HashSet::new();
331        loop {
332            if !visited.insert(current) {
333                return None;
334            }
335            let schema = self.analysis.schemas.get(current)?;
336            if let SchemaType::Reference { target } = &schema.schema_type {
337                current = target;
338            } else {
339                return Some(schema);
340            }
341        }
342    }
343
344    fn query_object_properties(
345        &self,
346        parameter: &ParameterInfo,
347    ) -> Option<&BTreeMap<String, crate::analysis::PropertyInfo>> {
348        let schema = self.resolve_query_schema(parameter.schema_ref.as_deref()?)?;
349        match &schema.schema_type {
350            SchemaType::Object { properties, .. } => Some(properties),
351            _ => None,
352        }
353    }
354
355    fn query_property_is_scalar(
356        &self,
357        schema_type: &SchemaType,
358        visited: &mut std::collections::HashSet<String>,
359    ) -> bool {
360        match schema_type {
361            SchemaType::Primitive { .. }
362            | SchemaType::StringEnum { .. }
363            | SchemaType::ExtensibleEnum { .. } => true,
364            SchemaType::Reference { target } if visited.insert(target.clone()) => {
365                self.analysis.schemas.get(target).is_some_and(|schema| {
366                    self.query_property_is_scalar(&schema.schema_type, visited)
367                })
368            }
369            _ => false,
370        }
371    }
372
373    fn validate_query_object(
374        &self,
375        operation: &OperationInfo,
376        parameter: &ParameterInfo,
377    ) -> Result<Vec<String>, ServerCodegenError> {
378        let error = |reason: String| ServerCodegenError::UnsupportedQueryParameter {
379            operation_id: operation.operation_id.clone(),
380            parameter: parameter.name.clone(),
381            reason,
382        };
383        let schema_name = parameter.schema_ref.as_deref().ok_or_else(|| {
384            error("styled object parameter has no analyzed schema type".to_string())
385        })?;
386        let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
387            error(format!(
388                "query object schema `{schema_name}` could not be resolved"
389            ))
390        })?;
391        let (properties, additional_properties) = match &schema.schema_type {
392            SchemaType::Object {
393                properties,
394                additional_properties,
395                ..
396            } => (properties, additional_properties),
397            _ => {
398                return Err(error(format!(
399                    "query schema `{schema_name}` does not resolve to a flat object"
400                )));
401            }
402        };
403        if !matches!(additional_properties, ObjectAdditionalProperties::Forbidden) {
404            return Err(error(
405                "styled object parameters with additionalProperties have an ambiguous wire namespace"
406                    .to_string(),
407            ));
408        }
409        for (property_name, property) in properties {
410            if !self.query_property_is_scalar(
411                &property.schema_type,
412                &mut std::collections::HashSet::new(),
413            ) {
414                return Err(error(format!(
415                    "property `{property_name}` is not scalar; nested arrays/objects are undefined for the generated query wire format"
416                )));
417            }
418        }
419        Ok(properties.keys().cloned().collect())
420    }
421
422    fn validate_query_parameters(
423        &self,
424        operations: &[&OperationInfo],
425    ) -> Result<(), ServerCodegenError> {
426        for operation in operations {
427            let mut claimed_keys: BTreeMap<String, String> = BTreeMap::new();
428            for parameter in operation
429                .parameters
430                .iter()
431                .filter(|parameter| parameter.location == "query")
432            {
433                let mut keys = match &parameter.query_serialization {
434                    Some(QuerySerialization::Unsupported { reason }) => {
435                        return Err(ServerCodegenError::UnsupportedQueryParameter {
436                            operation_id: operation.operation_id.clone(),
437                            parameter: parameter.name.clone(),
438                            reason: reason.clone(),
439                        });
440                    }
441                    Some(
442                        QuerySerialization::FormExplodedObject
443                        | QuerySerialization::FormObject
444                        | QuerySerialization::DeepObject,
445                    ) => {
446                        let property_keys = self.validate_query_object(operation, parameter)?;
447                        if matches!(
448                            parameter.query_serialization,
449                            Some(QuerySerialization::FormExplodedObject)
450                        ) {
451                            property_keys
452                        } else if matches!(
453                            parameter.query_serialization,
454                            Some(QuerySerialization::DeepObject)
455                        ) {
456                            property_keys
457                                .into_iter()
458                                .map(|property| format!("{}[{property}]", parameter.name))
459                                .collect()
460                        } else {
461                            vec![parameter.name.clone()]
462                        }
463                    }
464                    Some(
465                        QuerySerialization::FormExplodedArray { .. }
466                        | QuerySerialization::FormArray { .. },
467                    )
468                    | None => vec![parameter.name.clone()],
469                };
470                if matches!(
471                    &parameter.query_serialization,
472                    Some(
473                        QuerySerialization::FormExplodedObject
474                            | QuerySerialization::FormObject
475                            | QuerySerialization::DeepObject
476                            | QuerySerialization::FormExplodedArray { .. }
477                            | QuerySerialization::FormArray { .. }
478                    )
479                ) {
480                    keys.push(format!("{}[]", parameter.name));
481                }
482                for key in keys {
483                    if let Some(first_parameter) =
484                        claimed_keys.insert(key.clone(), parameter.name.clone())
485                    {
486                        return Err(ServerCodegenError::AmbiguousQueryParameter {
487                            operation_id: operation.operation_id.clone(),
488                            wire_key: key,
489                            first_parameter,
490                            second_parameter: parameter.name.clone(),
491                        });
492                    }
493                }
494            }
495        }
496        Ok(())
497    }
498
499    fn emit_mod(&self) -> TokenStream {
500        let provenance_attribute = self.provenance_attribute();
501        quote! {
502            //! Server scaffolding emitted by openapi-to-rust.
503            //!
504            //! Implement the per-tag trait(s) in `api` on your own struct,
505            //! then build an `axum::Router` via `router::router(impl)`.
506
507            #provenance_attribute
508
509            pub mod api;
510            pub mod errors;
511            pub mod router;
512
513            pub use api::*;
514            pub use errors::*;
515            pub use router::*;
516        }
517    }
518
519    fn emit_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
520        let provenance_attribute = self.provenance_attribute();
521        let factories: Vec<TokenStream> = groups
522            .iter()
523            .map(|(tag, ops)| self.emit_router_for_trait(tag, ops))
524            .collect();
525
526        // Per-op Query structs — one per op that has any query params.
527        let query_structs: Vec<TokenStream> = groups
528            .values()
529            .flatten()
530            .filter_map(|op| self.emit_query_struct(op))
531            .collect();
532        let has_query_parameters = groups.values().flatten().any(|operation| {
533            operation
534                .parameters
535                .iter()
536                .any(|parameter| parameter.location == "query")
537        });
538        let query_helpers = has_query_parameters.then(|| {
539            quote! {
540                fn __query_pairs(raw: ::std::option::Option<&str>) -> ::std::vec::Vec<(String, String)> {
541                    raw.map(|query| {
542                        ::url::form_urlencoded::parse(query.as_bytes())
543                            .into_owned()
544                            .collect()
545                    })
546                    .unwrap_or_default()
547                }
548
549                fn __query_one(
550                    pairs: &[(String, String)],
551                    key: &str,
552                ) -> ::std::result::Result<::std::option::Option<String>, String> {
553                    let mut values = pairs
554                        .iter()
555                        .filter(|(candidate, _)| candidate == key)
556                        .map(|(_, value)| value.clone());
557                    let value = values.next();
558                    if values.next().is_some() {
559                        return Err(format!("query parameter `{key}` appeared more than once"));
560                    }
561                    Ok(value)
562                }
563
564                fn __decode_query_scalar<T>(
565                    value: &str,
566                    label: &str,
567                ) -> ::std::result::Result<T, String>
568                where
569                    T: ::serde::de::DeserializeOwned,
570                {
571                    ::serde_json::from_value(::serde_json::Value::String(value.to_string()))
572                        .or_else(|_| ::serde_json::from_str(value))
573                        .map_err(|error| format!("invalid query value for `{label}`: {error}"))
574                }
575
576                fn __decode_query_object<T>(
577                    fields: &[(String, String)],
578                    label: &str,
579                ) -> ::std::result::Result<T, String>
580                where
581                    T: ::serde::de::DeserializeOwned,
582                {
583                    let mut serializer =
584                        ::url::form_urlencoded::Serializer::new(String::new());
585                    for (key, value) in fields {
586                        serializer.append_pair(key, value);
587                    }
588                    ::serde_urlencoded::from_str(&serializer.finish())
589                        .map_err(|error| format!("invalid query object `{label}`: {error}"))
590                }
591
592                fn __query_empty_marker(
593                    pairs: &[(String, String)],
594                    key: &str,
595                ) -> ::std::result::Result<bool, String> {
596                    let marker = format!("{key}[]");
597                    match __query_one(pairs, &marker)? {
598                        Some(value) if value.is_empty() => Ok(true),
599                        Some(_) => Err(format!(
600                            "zero-cardinality marker `{marker}` must have an empty value"
601                        )),
602                        None => Ok(false),
603                    }
604                }
605            }
606        });
607
608        // When the picked operations span multiple tags, emit a
609        // top-level `build_router(impl1, impl2, ...)` that takes one
610        // generic per trait and `.merge()`s the per-tag factories.
611        // For a single-tag selection this is unnecessary noise — the
612        // user calls the per-tag factory directly.
613        let combined = if groups.len() > 1 {
614            Some(self.emit_combined_router(groups))
615        } else {
616            None
617        };
618
619        quote! {
620            //! Router factories — one per trait. Each takes any
621            //! `T: <TraitName> + Clone + Send + Sync + 'static` and
622            //! returns an `axum::Router` with state pre-attached.
623
624            #provenance_attribute
625
626            use super::api::*;
627            use super::errors::*;
628            // Pull schemas directly from the types module (always a
629            // sibling of mod.rs). Doesn't rely on the parent module
630            // re-exporting types::*, so users can mount the generated
631            // tree at any path without rewriting these imports.
632            #[allow(unused_imports)]
633            use super::super::types::*;
634
635            #query_helpers
636
637            #(#query_structs)*
638
639            #(#factories)*
640
641            #combined
642        }
643    }
644
645    fn emit_combined_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
646        // Stable ordering: BTreeMap iteration is already alphabetical
647        // by tag, which gives us deterministic generic ordering across
648        // generator runs.
649        let entries: Vec<(syn::Ident, syn::Ident, syn::Ident)> = groups
650            .keys()
651            .enumerate()
652            .map(|(i, tag)| {
653                let trait_ident = trait_ident_for_tag(tag);
654                let factory = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
655                let generic = format_ident!("T{}", i + 1);
656                (trait_ident, factory, generic)
657            })
658            .collect();
659
660        let generics: Vec<&syn::Ident> = entries.iter().map(|(_, _, g)| g).collect();
661        let args: Vec<TokenStream> = entries
662            .iter()
663            .map(|(trait_ident, _, g)| {
664                let arg_ident = format_ident!("{}", trait_ident.to_string().to_snake_case());
665                quote! { #arg_ident: #g }
666            })
667            .collect();
668        let bounds: Vec<TokenStream> = entries
669            .iter()
670            .map(|(trait_ident, _, g)| {
671                quote! { #g: #trait_ident + Clone + Send + Sync + 'static }
672            })
673            .collect();
674
675        // Fold the factories: `factory1(arg1).merge(factory2(arg2)).merge(...)`.
676        let first = &entries[0];
677        let first_arg = format_ident!("{}", first.0.to_string().to_snake_case());
678        let first_factory = &first.1;
679        let rest = entries
680            .iter()
681            .skip(1)
682            .map(|(trait_ident, factory, _)| {
683                let arg = format_ident!("{}", trait_ident.to_string().to_snake_case());
684                quote! { .merge(#factory(#arg)) }
685            })
686            .collect::<Vec<_>>();
687
688        let trait_names: Vec<String> = entries.iter().map(|(t, _, _)| t.to_string()).collect();
689        let doc = format!(
690            " Combined router spanning {} traits: {}.",
691            entries.len(),
692            trait_names.join(", "),
693        );
694
695        quote! {
696            #[doc = #doc]
697            pub fn build_router<#(#generics),*>(
698                #(#args),*
699            ) -> ::axum::Router
700            where
701                #(#bounds),*
702            {
703                #first_factory(#first_arg) #(#rest)*
704            }
705        }
706    }
707
708    fn emit_router_for_trait(&self, tag: &str, ops: &[&OperationInfo]) -> TokenStream {
709        let trait_ident = trait_ident_for_tag(tag);
710        let fn_ident = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
711
712        let mut routes: Vec<TokenStream> = Vec::new();
713        let mut custom_by_path: BTreeMap<String, Vec<(String, syn::Ident)>> = BTreeMap::new();
714        for op in ops {
715            let handler = format_ident!("{}_handler", op.operation_id.to_snake_case());
716            let path = openapi_to_axum_path(&op.path);
717            if let Some(method_call) = axum_method_call(&op.method) {
718                routes.push(quote! { .route(#path, ::axum::routing::#method_call(#handler::<T>)) });
719            } else {
720                custom_by_path
721                    .entry(path)
722                    .or_default()
723                    .push((op.method.to_ascii_uppercase(), handler));
724            }
725        }
726        let mut custom_dispatchers = Vec::new();
727        for (path, methods) in custom_by_path {
728            let first_handler = &methods[0].1;
729            let dispatcher = format_ident!("{}_custom_method_dispatch", first_handler);
730            let (route, dispatcher_fn) =
731                axum_custom_route(&path, &dispatcher, &methods, &trait_ident);
732            routes.push(route);
733            custom_dispatchers.push(dispatcher_fn);
734        }
735
736        let handlers: Vec<TokenStream> = ops
737            .iter()
738            .map(|op| self.emit_axum_handler(&trait_ident, op))
739            .collect();
740
741        let doc = format!(" Build an axum::Router for the `{trait_ident}` trait.");
742
743        quote! {
744            #[doc = #doc]
745            pub fn #fn_ident<T>(api: T) -> ::axum::Router
746            where
747                T: #trait_ident + Clone + Send + Sync + 'static,
748            {
749                ::axum::Router::new()
750                    #(#routes)*
751                    .with_state(api)
752            }
753
754            #(#custom_dispatchers)*
755
756            #(#handlers)*
757        }
758    }
759
760    fn emit_axum_handler(&self, trait_ident: &syn::Ident, op: &OperationInfo) -> TokenStream {
761        let handler_ident = format_ident!("{}_handler", op.operation_id.to_snake_case());
762        let trait_method = format_ident!("{}", op.operation_id.to_snake_case());
763
764        // Build extractor list + call argument list.
765        let mut extractors: Vec<TokenStream> =
766            vec![quote! { ::axum::extract::State(api): ::axum::extract::State<T> }];
767        let mut call_args: Vec<TokenStream> = Vec::new();
768
769        // Path parameters → axum::extract::Path tuple
770        let path_params: Vec<&_> = op
771            .parameters
772            .iter()
773            .filter(|p| p.location == "path")
774            .collect();
775        if !path_params.is_empty() {
776            let idents: Vec<syn::Ident> = path_params
777                .iter()
778                .map(|p| self.parameter_ident(p))
779                .collect();
780            let types: Vec<TokenStream> = path_params
781                .iter()
782                .map(|p| self.query_parameter_type(p))
783                .collect();
784            if path_params.len() == 1 {
785                let i = &idents[0];
786                let t = &types[0];
787                extractors.push(quote! { ::axum::extract::Path(#i): ::axum::extract::Path<#t> });
788            } else {
789                extractors.push(quote! { ::axum::extract::Path((#(#idents),*)): ::axum::extract::Path<(#(#types),*)> });
790            }
791            for i in &idents {
792                call_args.push(quote! { #i });
793            }
794        }
795
796        // Query parameters — extract via a per-op `<Op>Query` struct
797        // (emitted in the same router.rs above). Required params are
798        // unwrapped here (short-circuit 400 if missing) so the trait
799        // method sees a `T` rather than `Option<T>`.
800        let query_params: Vec<&_> = op
801            .parameters
802            .iter()
803            .filter(|p| p.location == "query")
804            .collect();
805        let mut required_query_checks: Vec<TokenStream> = Vec::new();
806        let mut query_decode = TokenStream::new();
807        if !query_params.is_empty() {
808            let query_ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
809            let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
810            extractors.push(quote! {
811                ::axum::extract::RawQuery(__raw_query): ::axum::extract::RawQuery
812            });
813            query_decode = quote! {
814                let __q: #query_ident = match #decode_ident(__raw_query.as_deref()) {
815                    Ok(query) => query,
816                    Err(message) => return ::axum::response::IntoResponse::into_response(
817                        (
818                            ::axum::http::StatusCode::BAD_REQUEST,
819                            ::axum::Json(::serde_json::json!({ "error": message })),
820                        )
821                    ),
822                };
823            };
824            for p in &query_params {
825                let f = self.parameter_ident(p);
826                let wire = p.name.as_str();
827                if p.required {
828                    let missing_msg = format!("missing required query parameter `{wire}`");
829                    required_query_checks.push(quote! {
830                        let #f = match __q.#f {
831                            Some(v) => v,
832                            None => return ::axum::response::IntoResponse::into_response(
833                                (
834                                    ::axum::http::StatusCode::BAD_REQUEST,
835                                    ::axum::Json(::serde_json::json!({
836                                        "error": #missing_msg
837                                    })),
838                                )
839                            ),
840                        };
841                    });
842                    call_args.push(quote! { #f });
843                } else {
844                    call_args.push(quote! { __q.#f });
845                }
846            }
847        }
848
849        // Header parameters — extract via HeaderMap and read each
850        // header by name. Required headers short-circuit with 400 if
851        // missing or non-UTF-8.
852        let header_params: Vec<&_> = op
853            .parameters
854            .iter()
855            .filter(|p| p.location == "header")
856            .collect();
857        let mut required_header_checks: Vec<TokenStream> = Vec::new();
858        if !header_params.is_empty() {
859            extractors.push(quote! { __headers: ::axum::http::HeaderMap });
860            for p in &header_params {
861                let wire = p.name.as_str();
862                let ident = format_ident!("{}", header_param_ident(&p.name));
863                if p.required {
864                    let missing_msg = format!("missing required header `{wire}`");
865                    required_header_checks.push(quote! {
866                        let #ident = match __headers
867                            .get(#wire)
868                            .and_then(|v| v.to_str().ok())
869                            .map(::std::string::String::from)
870                        {
871                            Some(v) => v,
872                            None => return ::axum::response::IntoResponse::into_response(
873                                (
874                                    ::axum::http::StatusCode::BAD_REQUEST,
875                                    ::axum::Json(::serde_json::json!({
876                                        "error": #missing_msg
877                                    })),
878                                )
879                            ),
880                        };
881                    });
882                    call_args.push(quote! { #ident });
883                } else {
884                    call_args.push(quote! {
885                        __headers
886                            .get(#wire)
887                            .and_then(|v| v.to_str().ok())
888                            .map(::std::string::String::from)
889                    });
890                }
891            }
892        }
893
894        // Body
895        let body_ty_opt = body_type(op);
896        if let Some(body_ty) = &body_ty_opt {
897            let body_ty_tokens = parse_type(body_ty);
898            if op.request_body_required {
899                extractors.push(quote! {
900                    ::axum::Json(body): ::axum::Json<#body_ty_tokens>
901                });
902                call_args.push(quote! { body });
903            } else {
904                extractors.push(quote! {
905                    body: ::std::option::Option<::axum::Json<#body_ty_tokens>>
906                });
907                call_args.push(quote! { body.map(|::axum::Json(b)| b) });
908            }
909        }
910
911        let _ = format_ident!("{}Response", op.operation_id.to_pascal_case());
912        // Keep referencing trait_ident so the where-bound name is
913        // visible to downstream readers — clippy would otherwise flag
914        // it as unused in some configurations.
915        let _ = trait_ident;
916
917        // Handler returns `axum::response::Response` so the required-
918        // param short-circuit (400 BadRequest) and the trait method's
919        // typed response enum (via IntoResponse) can both flow out
920        // through the same return type.
921        quote! {
922            async fn #handler_ident<T>(
923                #(#extractors),*
924            ) -> ::axum::response::Response
925            where
926                T: super::api::#trait_ident + Clone + Send + Sync + 'static,
927            {
928                #query_decode
929                #(#required_query_checks)*
930                #(#required_header_checks)*
931                ::axum::response::IntoResponse::into_response(
932                    api.#trait_method(#(#call_args),*).await,
933                )
934            }
935        }
936    }
937
938    fn emit_api(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
939        let provenance_attribute = self.provenance_attribute();
940        let traits: Vec<TokenStream> = groups
941            .iter()
942            .map(|(tag, ops)| self.emit_trait(tag, ops))
943            .collect();
944
945        // Inline string enums declared on parameters get synthetic
946        // type names (e.g. `ListInputItemsOrder`). The analyzer
947        // surfaces enum_values; we emit the enum here so the trait
948        // signature compiles. Dedup by name in case two ops in the
949        // same picked set share the same synthetic name.
950        let mut emitted: std::collections::BTreeSet<String> = Default::default();
951        let mut param_enums: Vec<TokenStream> = Vec::new();
952        for op in groups.values().flatten() {
953            for p in &op.parameters {
954                if let Some(values) = &p.enum_values {
955                    if emitted.insert(p.rust_type.clone()) {
956                        param_enums.push(emit_param_enum(&p.rust_type, values));
957                    }
958                }
959            }
960        }
961
962        quote! {
963            //! Per-tag traits. Implement one of these on your own
964            //! struct; the router (P5) wires it into axum.
965
966            #provenance_attribute
967
968            #![allow(clippy::too_many_arguments)]
969
970            use super::errors::*;
971            // Schemas live in `<parent>/types.rs`. Reaching them via
972            // `super::super::types::*` instead of a glob on the
973            // parent module keeps these imports stable regardless of
974            // how the user mounts the generated tree.
975            #[allow(unused_imports)]
976            use super::super::types::*;
977
978            #(#param_enums)*
979
980            #(#traits)*
981        }
982    }
983
984    fn emit_trait(&self, tag: &str, ops: &[&OperationInfo]) -> TokenStream {
985        let trait_ident = trait_ident_for_tag(tag);
986        let methods: Vec<TokenStream> = ops.iter().map(|op| self.emit_method_sig(op)).collect();
987        let doc = format!(" Operations under the `{tag}` tag.");
988        quote! {
989            #[doc = #doc]
990            #[axum::async_trait]
991            pub trait #trait_ident: Send + Sync + 'static {
992                #(#methods)*
993            }
994        }
995    }
996
997    fn emit_method_sig(&self, op: &OperationInfo) -> TokenStream {
998        let name = format_ident!("{}", op.operation_id.to_snake_case());
999        let response_ty = format_ident!("{}Response", op.operation_id.to_pascal_case());
1000
1001        // Order: path → query → header → body. Required params keep
1002        // their declared rust_type; optional params wrap in Option<…>.
1003        // This mirrors what the router handler extracts so positional
1004        // ordering matches the call site exactly.
1005        let mut params: Vec<TokenStream> = Vec::new();
1006        for p in &op.parameters {
1007            if p.location == "path" {
1008                let ident = self.parameter_ident(p);
1009                let ty = self.query_parameter_type(p);
1010                params.push(quote! { #ident: #ty });
1011            }
1012        }
1013        for p in &op.parameters {
1014            if p.location == "query" {
1015                let ident = self.parameter_ident(p);
1016                let ty = self.query_parameter_type(p);
1017                // Required query params land as `T`; the handler
1018                // validates presence and returns 400 if absent, so
1019                // by the time the trait method sees the value it
1020                // must be Some. Optional → `Option<T>`.
1021                if p.required {
1022                    params.push(quote! { #ident: #ty });
1023                } else {
1024                    params.push(quote! { #ident: ::std::option::Option<#ty> });
1025                }
1026            }
1027        }
1028        for p in &op.parameters {
1029            if p.location == "header" {
1030                let ident = format_ident!("{}", header_param_ident(&p.name));
1031                if p.required {
1032                    params.push(quote! { #ident: String });
1033                } else {
1034                    params.push(quote! { #ident: ::std::option::Option<String> });
1035                }
1036            }
1037        }
1038        if let Some(body) = body_type(op) {
1039            let body_ty = parse_type(&body);
1040            if op.request_body_required {
1041                params.push(quote! { body: #body_ty });
1042            } else {
1043                params.push(quote! { body: Option<#body_ty> });
1044            }
1045        }
1046
1047        let summary_doc = op
1048            .summary
1049            .as_deref()
1050            .map(|s| format!(" {s}"))
1051            .unwrap_or_default();
1052        let route_doc = format!(" `{} {}`", op.method, op.path);
1053
1054        quote! {
1055            #[doc = #summary_doc]
1056            #[doc = ""]
1057            #[doc = #route_doc]
1058            async fn #name(&self, #(#params),*) -> #response_ty;
1059        }
1060    }
1061
1062    /// Per-op `<Op>Query` struct emitted into router.rs when the op
1063    /// has any query parameters. An operation-specific decoder fills it from
1064    /// Axum's raw query so repeated and structured keys remain observable.
1065    fn emit_query_struct(&self, op: &OperationInfo) -> Option<TokenStream> {
1066        let query_params: Vec<&_> = op
1067            .parameters
1068            .iter()
1069            .filter(|p| p.location == "query")
1070            .collect();
1071        if query_params.is_empty() {
1072            return None;
1073        }
1074        let ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
1075        let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
1076        let mut fields = Vec::new();
1077        let mut decoders = Vec::new();
1078        let mut field_idents = Vec::new();
1079        for parameter in query_params {
1080            let field_ident = self.parameter_ident(parameter);
1081            let field_type = self.query_parameter_type(parameter);
1082            let wire_name = parameter.name.as_str();
1083            fields.push(quote! {
1084                pub #field_ident: ::std::option::Option<#field_type>
1085            });
1086            field_idents.push(field_ident.clone());
1087
1088            let decoder = match &parameter.query_serialization {
1089                Some(QuerySerialization::FormExplodedArray { .. }) => quote! {
1090                    let #field_ident = {
1091                        let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
1092                        let raw_values: Vec<&str> = __pairs
1093                            .iter()
1094                            .filter(|(key, _)| key == #wire_name)
1095                            .map(|(_, value)| value.as_str())
1096                            .collect();
1097                        if empty_marker && !raw_values.is_empty() {
1098                            return Err(format!(
1099                                "query array `{}` cannot combine values with its empty marker",
1100                                #wire_name,
1101                            ));
1102                        }
1103                        if empty_marker {
1104                            Some(Vec::new())
1105                        } else if raw_values.is_empty() {
1106                            None
1107                        } else {
1108                            let mut values = Vec::with_capacity(raw_values.len());
1109                            for raw in raw_values {
1110                                values.push(__decode_query_scalar(raw, #wire_name)?);
1111                            }
1112                            Some(values)
1113                        }
1114                    };
1115                },
1116                Some(QuerySerialization::FormArray { .. }) => quote! {
1117                    let #field_ident = match (
1118                        __query_one(&__pairs, #wire_name)?,
1119                        __query_empty_marker(&__pairs, #wire_name)?,
1120                    ) {
1121                        (Some(_), true) => return Err(format!(
1122                            "query array `{}` cannot combine a value with its empty marker",
1123                            #wire_name,
1124                        )),
1125                        (Some(raw), false) => {
1126                            let mut values = Vec::new();
1127                            for item in raw.split(',') {
1128                                values.push(__decode_query_scalar(item, #wire_name)?);
1129                            }
1130                            Some(values)
1131                        }
1132                        (None, true) => Some(Vec::new()),
1133                        (None, false) => None,
1134                    };
1135                },
1136                Some(QuerySerialization::FormExplodedObject) => {
1137                    let property_names = self
1138                        .query_object_properties(parameter)
1139                        .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
1140                        .unwrap_or_default();
1141                    quote! {
1142                        let #field_ident = {
1143                            let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
1144                            let allowed = [#(#property_names),*];
1145                            let object_fields: Vec<(String, String)> = __pairs
1146                                .iter()
1147                                .filter(|(key, _)| allowed.contains(&key.as_str()))
1148                                .cloned()
1149                                .collect();
1150                            if empty_marker && !object_fields.is_empty() {
1151                                return Err(format!(
1152                                    "query object `{}` cannot combine properties with its empty marker",
1153                                    #wire_name,
1154                                ));
1155                            }
1156                            if object_fields.is_empty() && !empty_marker {
1157                                None
1158                            } else {
1159                                Some(__decode_query_object(&object_fields, #wire_name)?)
1160                            }
1161                        };
1162                    }
1163                }
1164                Some(QuerySerialization::FormObject) => quote! {
1165                    let #field_ident = match (
1166                        __query_one(&__pairs, #wire_name)?,
1167                        __query_empty_marker(&__pairs, #wire_name)?,
1168                    ) {
1169                        (Some(_), true) => return Err(format!(
1170                            "query object `{}` cannot combine a value with its empty marker",
1171                            #wire_name,
1172                        )),
1173                        (Some(raw), false) => {
1174                            let parts: Vec<&str> = raw.split(',').collect();
1175                            if parts.len() % 2 != 0 {
1176                                return Err(format!(
1177                                    "query object `{}` must contain alternating key,value entries",
1178                                    #wire_name,
1179                                ));
1180                            }
1181                            let object_fields: Vec<(String, String)> = parts
1182                                .chunks_exact(2)
1183                                .map(|pair| (pair[0].to_string(), pair[1].to_string()))
1184                                .collect();
1185                            Some(__decode_query_object(&object_fields, #wire_name)?)
1186                        }
1187                        (None, true) => Some(__decode_query_object(&[], #wire_name)?),
1188                        (None, false) => None,
1189                    };
1190                },
1191                Some(QuerySerialization::DeepObject) => {
1192                    let property_names = self
1193                        .query_object_properties(parameter)
1194                        .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
1195                        .unwrap_or_default();
1196                    quote! {
1197                        let #field_ident = {
1198                            let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
1199                            let prefix = format!("{}[", #wire_name);
1200                            let allowed = [#(#property_names),*];
1201                            let mut object_fields = Vec::new();
1202                            for (key, value) in &__pairs {
1203                                if let Some(property) = key
1204                                    .strip_prefix(&prefix)
1205                                    .and_then(|rest| rest.strip_suffix(']'))
1206                                {
1207                                    if property.is_empty() {
1208                                        continue;
1209                                    }
1210                                    if !allowed.contains(&property) {
1211                                        return Err(format!(
1212                                            "unknown deepObject property `{}[{}]`",
1213                                            #wire_name,
1214                                            property,
1215                                        ));
1216                                    }
1217                                    object_fields.push((property.to_string(), value.clone()));
1218                                }
1219                            }
1220                            if empty_marker && !object_fields.is_empty() {
1221                                return Err(format!(
1222                                    "query object `{}` cannot combine properties with its empty marker",
1223                                    #wire_name,
1224                                ));
1225                            }
1226                            if object_fields.is_empty() && !empty_marker {
1227                                None
1228                            } else {
1229                                Some(__decode_query_object(&object_fields, #wire_name)?)
1230                            }
1231                        };
1232                    }
1233                }
1234                Some(QuerySerialization::Unsupported { .. }) | None => quote! {
1235                    let #field_ident = __query_one(&__pairs, #wire_name)?
1236                        .map(|raw| __decode_query_scalar(&raw, #wire_name))
1237                        .transpose()?;
1238                },
1239            };
1240            decoders.push(decoder);
1241        }
1242        let doc = format!(
1243            " Query parameters for `{} {}` (operationId `{}`).",
1244            op.method, op.path, op.operation_id
1245        );
1246        Some(quote! {
1247            #[doc = #doc]
1248            #[derive(Debug, Default)]
1249            pub struct #ident {
1250                #(#fields),*
1251            }
1252
1253            fn #decode_ident(
1254                raw: ::std::option::Option<&str>,
1255            ) -> ::std::result::Result<#ident, String> {
1256                let __pairs = __query_pairs(raw);
1257                #(#decoders)*
1258                Ok(#ident {
1259                    #(#field_idents),*
1260                })
1261            }
1262        })
1263    }
1264
1265    fn emit_errors(&self, ops: &[&OperationInfo]) -> TokenStream {
1266        let provenance_attribute = self.provenance_attribute();
1267        let any_streaming = ops.iter().any(|op| op.supports_streaming);
1268        let enums: Vec<TokenStream> = ops.iter().map(|op| self.emit_response_enum(op)).collect();
1269
1270        // The SSE type alias is emitted exactly when at least one
1271        // picked op streams. Bringing it in unconditionally would force
1272        // `futures-core` into the user's dep tree even when they don't
1273        // need it.
1274        let stream_alias = if any_streaming {
1275            quote! {
1276                /// Stream payload carried by `*Stream` variants. Each
1277                /// yielded item is a pre-built `axum::response::sse::Event`.
1278                pub type ServerEventStream = ::std::pin::Pin<
1279                    Box<
1280                        dyn ::futures_core::Stream<
1281                                Item = ::std::result::Result<
1282                                    ::axum::response::sse::Event,
1283                                    ::std::convert::Infallible,
1284                                >,
1285                            > + ::std::marker::Send
1286                            + 'static,
1287                    >,
1288                >;
1289
1290                /// Wrap any `Stream<Item = Result<Event, Infallible>>` in
1291                /// a `Sse<ServerEventStream>` ready to drop into the
1292                /// `OkStream` variant. Replaces the
1293                /// `Sse::new(Box::pin(...))` dance.
1294                pub fn sse_response<S>(stream: S) -> ::axum::response::sse::Sse<ServerEventStream>
1295                where
1296                    S: ::futures_core::Stream<
1297                            Item = ::std::result::Result<
1298                                ::axum::response::sse::Event,
1299                                ::std::convert::Infallible,
1300                            >,
1301                        > + ::std::marker::Send
1302                        + 'static,
1303                {
1304                    ::axum::response::sse::Sse::new(Box::pin(stream))
1305                }
1306            }
1307        } else {
1308            quote! {}
1309        };
1310
1311        // We deliberately do NOT import `axum::response::Response` here:
1312        // many specs declare a schema literally named `Response`
1313        // (OpenAI's `createResponse` is one such case), and an explicit
1314        // import would shadow the glob-imported schema name. The
1315        // IntoResponse impl returns `axum::response::Response`
1316        // fully qualified.
1317        quote! {
1318            //! Per-operation response enums. Pick a variant to pick a
1319            //! status code — IntoResponse maps each variant to its
1320            //! documented (StatusCode, Json) pair.
1321
1322            #provenance_attribute
1323
1324            #![allow(clippy::large_enum_variant)]
1325
1326            use axum::{
1327                http::StatusCode,
1328                response::IntoResponse,
1329                Json,
1330            };
1331            // Schemas live in `<parent>/types.rs`. Reaching them via
1332            // `super::super::types::*` instead of a glob on the
1333            // parent module keeps these imports stable regardless of
1334            // how the user mounts the generated tree.
1335            #[allow(unused_imports)]
1336            use super::super::types::*;
1337
1338            #stream_alias
1339
1340            #(#enums)*
1341        }
1342    }
1343
1344    fn emit_response_enum(&self, op: &OperationInfo) -> TokenStream {
1345        let enum_ident = format_ident!("{}Response", op.operation_id.to_pascal_case());
1346        let mut variants: Vec<TokenStream> = Vec::new();
1347        let mut arms: Vec<TokenStream> = Vec::new();
1348
1349        for (status, schema_name) in &op.response_schemas {
1350            let variant = format_ident!("{}", status_variant_name(status));
1351            let body_ty = parse_type(schema_name);
1352            variants.push(quote! { #variant(#body_ty) });
1353            let status_expr = status_token(status);
1354            arms.push(quote! {
1355                Self::#variant(body) => (#status_expr, Json(body)).into_response()
1356            });
1357        }
1358
1359        // SSE: when the operation declares text/event-stream on any
1360        // response, add a streaming sibling variant. The user picks
1361        // it when their request had `stream: true` (or whatever the
1362        // streaming trigger is). Variant payload is a fully built
1363        // `axum::Sse` so the user controls keep-alive, retry interval,
1364        // etc.
1365        if op.supports_streaming {
1366            variants.push(quote! {
1367                OkStream(::axum::response::sse::Sse<ServerEventStream>)
1368            });
1369            arms.push(quote! {
1370                Self::OkStream(sse) => sse.into_response()
1371            });
1372        }
1373
1374        // Fallback: if no response variants were declared we still need
1375        // a no-op enum so the trait method has a return type. Use an
1376        // empty `Empty` variant returning 204.
1377        if variants.is_empty() {
1378            variants.push(quote! { Empty });
1379            arms.push(quote! {
1380                Self::Empty => StatusCode::NO_CONTENT.into_response()
1381            });
1382        }
1383
1384        let doc = format!(
1385            " Response for `{} {}` (operationId `{}`).",
1386            op.method, op.path, op.operation_id
1387        );
1388
1389        quote! {
1390            #[doc = #doc]
1391            pub enum #enum_ident {
1392                #(#variants),*
1393            }
1394
1395            impl IntoResponse for #enum_ident {
1396                fn into_response(self) -> ::axum::response::Response {
1397                    match self {
1398                        #(#arms),*
1399                    }
1400                }
1401            }
1402        }
1403    }
1404}
1405
1406/// Convert a wire-level header name (e.g. `anthropic-version`,
1407/// `X-Request-Id`) to a Rust identifier (`anthropic_version`,
1408/// `x_request_id`). Snake_case lowercases + replaces hyphens.
1409fn header_param_ident(name: &str) -> String {
1410    name.replace('-', "_").to_snake_case()
1411}
1412
1413/// Emit a string-enum type for a parameter whose inline schema
1414/// declared `enum: [...]`. The analyzer sets `rust_type` to a
1415/// synthetic name (`{OpId}{Param}` in PascalCase) and surfaces the
1416/// values; the codegen layer is what actually writes the enum.
1417fn emit_param_enum(name: &str, values: &[String]) -> TokenStream {
1418    let enum_ident = format_ident!("{}", name);
1419    let variants: Vec<TokenStream> = values
1420        .iter()
1421        .enumerate()
1422        .map(|(i, raw)| {
1423            let pascal = raw.to_pascal_case();
1424            // PascalCase can produce an empty string (pure-symbol
1425            // input) or an identifier starting with a digit
1426            // (e.g. `1d` stays `1d`) — both invalid as Rust idents.
1427            // Fall back to a positional name so the enum compiles.
1428            let starts_with_digit = pascal
1429                .chars()
1430                .next()
1431                .map(|c| c.is_ascii_digit())
1432                .unwrap_or(true);
1433            let v_name = if pascal.is_empty() || starts_with_digit {
1434                format!("Variant{i}")
1435            } else {
1436                pascal
1437            };
1438            let v_ident = format_ident!("{}", v_name);
1439            let default_marker = if i == 0 {
1440                quote! { #[default] }
1441            } else {
1442                quote! {}
1443            };
1444            quote! {
1445                #default_marker
1446                #[serde(rename = #raw)]
1447                #v_ident
1448            }
1449        })
1450        .collect();
1451    quote! {
1452        #[derive(Debug, Clone, PartialEq, Eq, ::serde::Deserialize, ::serde::Serialize, Default)]
1453        pub enum #enum_ident {
1454            #(#variants),*
1455        }
1456    }
1457}
1458
1459fn axum_method_call(method: &str) -> Option<TokenStream> {
1460    match method.to_ascii_uppercase().as_str() {
1461        "CONNECT" => Some(quote! { connect }),
1462        "DELETE" => Some(quote! { delete }),
1463        "GET" => Some(quote! { get }),
1464        "HEAD" => Some(quote! { head }),
1465        "OPTIONS" => Some(quote! { options }),
1466        "PATCH" => Some(quote! { patch }),
1467        "POST" => Some(quote! { post }),
1468        "PUT" => Some(quote! { put }),
1469        "TRACE" => Some(quote! { trace }),
1470        _ => None,
1471    }
1472}
1473
1474/// Build one exact-method dispatcher for every nonstandard operation sharing a
1475/// path within one generated trait. Axum has convenience functions for the
1476/// standard RFC methods, but OpenAPI 3.2 also defines QUERY and permits custom
1477/// `additionalOperations`. Axum allows only one `any` fallback per path, so all
1478/// custom methods on that path and trait must share this dispatcher. Generation
1479/// rejects the cross-trait form before reaching this helper.
1480fn axum_custom_route(
1481    path: &str,
1482    dispatcher: &syn::Ident,
1483    methods: &[(String, syn::Ident)],
1484    trait_ident: &syn::Ident,
1485) -> (TokenStream, TokenStream) {
1486    let arms = methods.iter().map(|(method, handler)| {
1487        quote! {
1488            #method => ::axum::handler::Handler::call(#handler::<T>, request, api).await
1489        }
1490    });
1491    let route = quote! {
1492        .route(#path, ::axum::routing::any(#dispatcher::<T>))
1493    };
1494    let dispatcher_fn = quote! {
1495        async fn #dispatcher<T>(
1496            ::axum::extract::State(api): ::axum::extract::State<T>,
1497            request: ::axum::extract::Request,
1498        ) -> ::axum::response::Response
1499        where
1500            T: #trait_ident + Clone + Send + Sync + 'static,
1501        {
1502            match request.method().as_str() {
1503                #(#arms,)*
1504                _ => ::axum::response::IntoResponse::into_response(
1505                    ::axum::http::StatusCode::METHOD_NOT_ALLOWED,
1506                ),
1507            }
1508        }
1509    };
1510    (route, dispatcher_fn)
1511}
1512
1513/// OpenAPI uses `{param}` placeholders; axum 0.7 accepts the same
1514/// `{param}` syntax for typed extraction, so this is currently a
1515/// pass-through. The helper exists so future syntax shifts (e.g.
1516/// nested wildcards) live in one place.
1517fn openapi_to_axum_path(p: &str) -> String {
1518    p.to_string()
1519}
1520
1521fn body_type(op: &OperationInfo) -> Option<String> {
1522    match &op.request_body {
1523        Some(RequestBodyContent::Json { schema_name })
1524        | Some(RequestBodyContent::FormUrlEncoded { schema_name }) => Some(schema_name.clone()),
1525        _ => None,
1526    }
1527}
1528
1529fn group_by_tag<'a>(ops: &[&'a OperationInfo]) -> BTreeMap<String, Vec<&'a OperationInfo>> {
1530    let mut groups: BTreeMap<String, Vec<&OperationInfo>> = BTreeMap::new();
1531    for op in ops {
1532        let tag = primary_tag(op);
1533        groups.entry(tag).or_default().push(op);
1534    }
1535    groups
1536}
1537
1538fn primary_tag(op: &OperationInfo) -> String {
1539    op.tags.first().cloned().unwrap_or_else(|| "Server".into())
1540}
1541
1542fn validate_custom_method_route_groups(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
1543    let mut tags_by_path: BTreeMap<&str, std::collections::BTreeSet<String>> = BTreeMap::new();
1544    for op in ops {
1545        if axum_method_call(&op.method).is_none() {
1546            tags_by_path
1547                .entry(&op.path)
1548                .or_default()
1549                .insert(primary_tag(op));
1550        }
1551    }
1552    if let Some((path, tags)) = tags_by_path.into_iter().find(|(_, tags)| tags.len() > 1) {
1553        return Err(ServerCodegenError::CrossTagCustomMethods {
1554            path: path.to_string(),
1555            tags: tags.into_iter().collect::<Vec<_>>().join(", "),
1556        });
1557    }
1558    Ok(())
1559}
1560
1561fn trait_ident_for_tag(tag: &str) -> syn::Ident {
1562    let pascal = tag.to_pascal_case();
1563    let base = if pascal.is_empty() {
1564        "Server".into()
1565    } else {
1566        pascal
1567    };
1568    format_ident!("{}Api", base)
1569}
1570
1571/// Convert a status code (or `default`, or wildcard `4XX`) to a
1572/// variant identifier.
1573fn status_variant_name(status: &str) -> String {
1574    match status {
1575        "200" => "Ok".into(),
1576        "201" => "Created".into(),
1577        "202" => "Accepted".into(),
1578        "204" => "NoContent".into(),
1579        "301" => "MovedPermanently".into(),
1580        "302" => "Found".into(),
1581        "304" => "NotModified".into(),
1582        "400" => "BadRequest".into(),
1583        "401" => "Unauthorized".into(),
1584        "403" => "Forbidden".into(),
1585        "404" => "NotFound".into(),
1586        "409" => "Conflict".into(),
1587        "410" => "Gone".into(),
1588        "422" => "UnprocessableEntity".into(),
1589        "429" => "TooManyRequests".into(),
1590        "500" => "InternalServerError".into(),
1591        "502" => "BadGateway".into(),
1592        "503" => "ServiceUnavailable".into(),
1593        "default" => "Default".into(),
1594        "2XX" => "Success".into(),
1595        "3XX" => "Redirection".into(),
1596        "4XX" => "ClientError".into(),
1597        "5XX" => "ServerError".into(),
1598        other => format!("Status{}", other.to_ascii_uppercase().replace('X', "x")),
1599    }
1600}
1601
1602/// Emit a StatusCode expression for a status string. Numeric codes use
1603/// the named constants where possible; wildcard ranges and `default`
1604/// pick a representative code (the lowest in-range).
1605fn status_token(status: &str) -> TokenStream {
1606    match status {
1607        "200" => quote! { StatusCode::OK },
1608        "201" => quote! { StatusCode::CREATED },
1609        "202" => quote! { StatusCode::ACCEPTED },
1610        "204" => quote! { StatusCode::NO_CONTENT },
1611        "301" => quote! { StatusCode::MOVED_PERMANENTLY },
1612        "302" => quote! { StatusCode::FOUND },
1613        "304" => quote! { StatusCode::NOT_MODIFIED },
1614        "400" => quote! { StatusCode::BAD_REQUEST },
1615        "401" => quote! { StatusCode::UNAUTHORIZED },
1616        "403" => quote! { StatusCode::FORBIDDEN },
1617        "404" => quote! { StatusCode::NOT_FOUND },
1618        "409" => quote! { StatusCode::CONFLICT },
1619        "410" => quote! { StatusCode::GONE },
1620        "422" => quote! { StatusCode::UNPROCESSABLE_ENTITY },
1621        "429" => quote! { StatusCode::TOO_MANY_REQUESTS },
1622        "500" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
1623        "502" => quote! { StatusCode::BAD_GATEWAY },
1624        "503" => quote! { StatusCode::SERVICE_UNAVAILABLE },
1625        "default" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
1626        "2XX" => quote! { StatusCode::OK },
1627        "3XX" => quote! { StatusCode::MOVED_PERMANENTLY },
1628        "4XX" => quote! { StatusCode::BAD_REQUEST },
1629        "5XX" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
1630        // Specific numeric codes not in our table — fall back to
1631        // StatusCode::from_u16. Codegen ensures a panic-free path by
1632        // unwrapping on a value that must parse (we already
1633        // know the spec wrote a numeric status here).
1634        other => {
1635            if let Ok(n) = other.parse::<u16>() {
1636                quote! {
1637                    StatusCode::from_u16(#n).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
1638                }
1639            } else {
1640                quote! { StatusCode::INTERNAL_SERVER_ERROR }
1641            }
1642        }
1643    }
1644}
1645
1646fn parse_type(ty: &str) -> TokenStream {
1647    syn::parse_str::<syn::Type>(ty)
1648        .map(|t| quote! { #t })
1649        .unwrap_or_else(|_| {
1650            let ident = format_ident!("{}", ty);
1651            quote! { #ident }
1652        })
1653}
1654
1655fn format_or_raw(ts: TokenStream) -> String {
1656    let raw = ts.to_string();
1657    match syn::parse_file(&raw) {
1658        Ok(parsed) => prettyplease::unparse(&parsed),
1659        Err(_) => raw,
1660    }
1661}
1662
1663#[cfg(test)]
1664mod tests {
1665    use super::*;
1666
1667    #[test]
1668    fn status_variant_name_maps_known_codes() {
1669        assert_eq!(status_variant_name("200"), "Ok");
1670        assert_eq!(status_variant_name("4XX"), "ClientError");
1671        assert_eq!(status_variant_name("default"), "Default");
1672        assert_eq!(status_variant_name("418"), "Status418");
1673    }
1674
1675    #[test]
1676    fn trait_ident_for_tag_appends_api() {
1677        let id = trait_ident_for_tag("Responses");
1678        assert_eq!(id.to_string(), "ResponsesApi");
1679    }
1680
1681    #[test]
1682    fn untagged_falls_back_to_server_api() {
1683        let id = trait_ident_for_tag("");
1684        assert_eq!(id.to_string(), "ServerApi");
1685    }
1686
1687    #[test]
1688    fn custom_methods_on_one_path_share_an_exact_dispatcher() {
1689        let dispatcher = format_ident!("cache_custom_method_dispatch");
1690        let methods = vec![
1691            ("PURGE".to_string(), format_ident!("purge_cache_handler")),
1692            ("QUERY".to_string(), format_ident!("query_cache_handler")),
1693        ];
1694        let trait_ident = format_ident!("CacheApi");
1695        let (route, dispatcher) = axum_custom_route("/cache", &dispatcher, &methods, &trait_ident);
1696        let route = route.to_string();
1697        let dispatcher = dispatcher.to_string();
1698        assert!(route.contains("routing :: any"));
1699        assert_eq!(route.matches("routing :: any").count(), 1);
1700        assert!(dispatcher.contains("\"PURGE\""));
1701        assert!(dispatcher.contains("\"QUERY\""));
1702        assert!(dispatcher.contains("purge_cache_handler"));
1703        assert!(dispatcher.contains("query_cache_handler"));
1704        assert!(dispatcher.contains("METHOD_NOT_ALLOWED"));
1705    }
1706
1707    #[test]
1708    fn standard_methods_use_axum_method_routes_without_guards() {
1709        assert!(axum_method_call("TRACE").is_some());
1710        assert!(axum_method_call("QUERY").is_none());
1711    }
1712}