1use 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
24pub fn reachable_schemas(
39 analysis: &SchemaAnalysis,
40 ops: &[&OperationInfo],
41) -> std::collections::BTreeSet<String> {
42 reachable_schemas_with_roots(analysis, ops, &[])
43}
44
45pub 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 collect_refs(&schema.original, &mut queue, &mut keep);
97 for dep in &schema.dependencies {
102 seed(dep, &mut queue, &mut keep);
103 }
104 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 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 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 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 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 ¶meter.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 ¶meter.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 #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 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 let combined = if groups.len() > 1 {
614 Some(self.emit_combined_router(groups))
615 } else {
616 None
617 };
618
619 quote! {
620 #provenance_attribute
625
626 use super::api::*;
627 use super::errors::*;
628 #[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 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 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 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 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 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 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 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 let _ = trait_ident;
916
917 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 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 #provenance_attribute
967
968 #![allow(clippy::too_many_arguments)]
969
970 use super::errors::*;
971 #[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 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 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 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 ¶meter.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 let stream_alias = if any_streaming {
1275 quote! {
1276 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 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 quote! {
1318 #provenance_attribute
1323
1324 #![allow(clippy::large_enum_variant)]
1325
1326 use axum::{
1327 http::StatusCode,
1328 response::IntoResponse,
1329 Json,
1330 };
1331 #[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 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 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
1406fn header_param_ident(name: &str) -> String {
1410 name.replace('-', "_").to_snake_case()
1411}
1412
1413fn 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 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
1474fn 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
1513fn 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
1571fn 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
1602fn 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 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}