1use crate::analysis::{OperationInfo, RequestBodyContent, SchemaAnalysis};
11use crate::config::ServerSection;
12use crate::generator::{GeneratedFile, GeneratorConfig};
13
14use super::{OperationIndex, Selector};
15use heck::{ToPascalCase, ToSnakeCase};
16use proc_macro2::TokenStream;
17use quote::{format_ident, quote};
18use std::collections::BTreeMap;
19use std::path::PathBuf;
20
21pub fn reachable_schemas(
36 analysis: &SchemaAnalysis,
37 ops: &[&OperationInfo],
38) -> std::collections::BTreeSet<String> {
39 let mut keep: std::collections::BTreeSet<String> = Default::default();
40 let mut queue: Vec<String> = Vec::new();
41
42 let seed =
43 |name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
44 if !name.is_empty() && keep.insert(name.to_string()) {
45 queue.push(name.to_string());
46 }
47 };
48
49 for op in ops {
50 if let Some(rb) = &op.request_body
51 && let Some(name) = rb.schema_name()
52 {
53 seed(name, &mut queue, &mut keep);
54 }
55 for ty in op.response_schemas.values() {
56 seed(ty, &mut queue, &mut keep);
57 }
58 for p in &op.parameters {
59 if let Some(name) = &p.schema_ref {
60 seed(name, &mut queue, &mut keep);
61 }
62 }
63 }
64
65 let ref_named = collect_all_ref_names(analysis);
80 for name in analysis.schemas.keys() {
81 if !ref_named.contains(name) && keep.insert(name.clone()) {
82 queue.push(name.clone());
83 }
84 }
85
86 while let Some(name) = queue.pop() {
87 if let Some(schema) = analysis.schemas.get(&name) {
88 collect_refs(&schema.original, &mut queue, &mut keep);
91 for dep in &schema.dependencies {
96 seed(dep, &mut queue, &mut keep);
97 }
98 }
99 }
100
101 keep
102}
103
104fn collect_all_ref_names(analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
109 let mut out: std::collections::HashSet<String> = Default::default();
110 for schema in analysis.schemas.values() {
111 gather_refs(&schema.original, &mut out);
112 }
113 for op in analysis.operations.values() {
118 if let Some(rb) = &op.request_body
119 && let Some(name) = rb.schema_name()
120 {
121 out.insert(name.to_string());
122 }
123 for ty in op.response_schemas.values() {
124 out.insert(ty.clone());
125 }
126 for p in &op.parameters {
127 if let Some(name) = &p.schema_ref {
128 out.insert(name.clone());
129 }
130 }
131 }
132 out
133}
134
135fn gather_refs(value: &serde_json::Value, out: &mut std::collections::HashSet<String>) {
136 match value {
137 serde_json::Value::Object(map) => {
138 for (k, v) in map {
139 if k == "$ref"
140 && let Some(s) = v.as_str()
141 && let Some(name) = s.strip_prefix("#/components/schemas/")
142 {
143 out.insert(name.to_string());
144 }
145 gather_refs(v, out);
146 }
147 }
148 serde_json::Value::Array(items) => items.iter().for_each(|v| gather_refs(v, out)),
149 _ => {}
150 }
151}
152
153fn collect_refs(
154 value: &serde_json::Value,
155 queue: &mut Vec<String>,
156 keep: &mut std::collections::BTreeSet<String>,
157) {
158 match value {
159 serde_json::Value::Object(map) => {
160 for (k, v) in map {
161 if k == "$ref"
162 && let Some(s) = v.as_str()
163 && let Some(name) = s.strip_prefix("#/components/schemas/")
164 && keep.insert(name.to_string())
165 {
166 queue.push(name.to_string());
167 }
168 collect_refs(v, queue, keep);
169 }
170 }
171 serde_json::Value::Array(items) => {
172 for v in items {
173 collect_refs(v, queue, keep);
174 }
175 }
176 _ => {}
177 }
178}
179
180#[derive(Debug, thiserror::Error)]
181pub enum ServerCodegenError {
182 #[error("server selector: {0}")]
183 Parse(#[from] super::SelectorParseError),
184 #[error("server selector: {0}")]
185 Resolve(#[from] super::SelectorResolveError),
186 #[error("internal: {0}")]
187 Internal(String),
188}
189
190pub struct ServerCodegen<'a> {
191 config: &'a GeneratorConfig,
192 analysis: &'a SchemaAnalysis,
193 server: &'a ServerSection,
194}
195
196impl<'a> ServerCodegen<'a> {
197 pub fn new(
198 config: &'a GeneratorConfig,
199 analysis: &'a SchemaAnalysis,
200 server: &'a ServerSection,
201 ) -> Self {
202 Self {
203 config,
204 analysis,
205 server,
206 }
207 }
208
209 pub fn generate(&self) -> Result<Vec<GeneratedFile>, ServerCodegenError> {
211 if self.server.operations.is_empty() {
212 return Ok(Vec::new());
213 }
214
215 let index = OperationIndex::from_analysis(self.analysis);
216 let selectors: Vec<Selector> = self
217 .server
218 .operations
219 .iter()
220 .map(|s| Selector::parse(s))
221 .collect::<Result<_, _>>()?;
222 let resolution = super::resolve(&selectors, &index)?;
223
224 let ops: Vec<&OperationInfo> = resolution
228 .operations
229 .iter()
230 .map(|s| {
231 self.analysis
232 .operations
233 .get(&s.operation_id)
234 .ok_or_else(|| {
235 ServerCodegenError::Internal(format!(
236 "operation `{}` resolved but missing from analysis",
237 s.operation_id
238 ))
239 })
240 })
241 .collect::<Result<_, _>>()?;
242
243 let groups = group_by_tag(&ops);
245
246 let api_rs = self.emit_api(&groups);
247 let errors_rs = self.emit_errors(&ops);
248 let router_rs = self.emit_router(&groups);
249 let mod_rs = self.emit_mod();
250
251 Ok(vec![
252 GeneratedFile {
253 path: PathBuf::from("server").join("mod.rs"),
254 content: format_or_raw(mod_rs),
255 },
256 GeneratedFile {
257 path: PathBuf::from("server").join("api.rs"),
258 content: format_or_raw(api_rs),
259 },
260 GeneratedFile {
261 path: PathBuf::from("server").join("errors.rs"),
262 content: format_or_raw(errors_rs),
263 },
264 GeneratedFile {
265 path: PathBuf::from("server").join("router.rs"),
266 content: format_or_raw(router_rs),
267 },
268 ])
269 }
270
271 fn emit_mod(&self) -> TokenStream {
272 let _ = &self.config; quote! {
274 pub mod api;
280 pub mod errors;
281 pub mod router;
282
283 pub use api::*;
284 pub use errors::*;
285 pub use router::*;
286 }
287 }
288
289 fn emit_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
290 let factories: Vec<TokenStream> = groups
291 .iter()
292 .map(|(tag, ops)| self.emit_router_for_trait(tag, ops))
293 .collect();
294
295 let query_structs: Vec<TokenStream> = groups
297 .values()
298 .flatten()
299 .filter_map(|op| self.emit_query_struct(op))
300 .collect();
301
302 let combined = if groups.len() > 1 {
308 Some(self.emit_combined_router(groups))
309 } else {
310 None
311 };
312
313 quote! {
314 use super::api::*;
319 use super::errors::*;
320 #[allow(unused_imports)]
325 use super::super::types::*;
326
327 #(#query_structs)*
328
329 #(#factories)*
330
331 #combined
332 }
333 }
334
335 fn emit_combined_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
336 let entries: Vec<(syn::Ident, syn::Ident, syn::Ident)> = groups
340 .keys()
341 .enumerate()
342 .map(|(i, tag)| {
343 let trait_ident = trait_ident_for_tag(tag);
344 let factory = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
345 let generic = format_ident!("T{}", i + 1);
346 (trait_ident, factory, generic)
347 })
348 .collect();
349
350 let generics: Vec<&syn::Ident> = entries.iter().map(|(_, _, g)| g).collect();
351 let args: Vec<TokenStream> = entries
352 .iter()
353 .map(|(trait_ident, _, g)| {
354 let arg_ident = format_ident!("{}", trait_ident.to_string().to_snake_case());
355 quote! { #arg_ident: #g }
356 })
357 .collect();
358 let bounds: Vec<TokenStream> = entries
359 .iter()
360 .map(|(trait_ident, _, g)| {
361 quote! { #g: #trait_ident + Clone + Send + Sync + 'static }
362 })
363 .collect();
364
365 let first = &entries[0];
367 let first_arg = format_ident!("{}", first.0.to_string().to_snake_case());
368 let first_factory = &first.1;
369 let rest = entries
370 .iter()
371 .skip(1)
372 .map(|(trait_ident, factory, _)| {
373 let arg = format_ident!("{}", trait_ident.to_string().to_snake_case());
374 quote! { .merge(#factory(#arg)) }
375 })
376 .collect::<Vec<_>>();
377
378 let trait_names: Vec<String> = entries.iter().map(|(t, _, _)| t.to_string()).collect();
379 let doc = format!(
380 " Combined router spanning {} traits: {}.",
381 entries.len(),
382 trait_names.join(", "),
383 );
384
385 quote! {
386 #[doc = #doc]
387 pub fn build_router<#(#generics),*>(
388 #(#args),*
389 ) -> ::axum::Router
390 where
391 #(#bounds),*
392 {
393 #first_factory(#first_arg) #(#rest)*
394 }
395 }
396 }
397
398 fn emit_router_for_trait(&self, tag: &str, ops: &[&OperationInfo]) -> TokenStream {
399 let trait_ident = trait_ident_for_tag(tag);
400 let fn_ident = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
401
402 let routes: Vec<TokenStream> = ops
403 .iter()
404 .map(|op| {
405 let method = axum_method_call(&op.method);
406 let handler = format_ident!("{}_handler", op.operation_id.to_snake_case());
407 let path = openapi_to_axum_path(&op.path);
408 quote! { .route(#path, ::axum::routing::#method(#handler::<T>)) }
409 })
410 .collect();
411
412 let handlers: Vec<TokenStream> = ops
413 .iter()
414 .map(|op| self.emit_axum_handler(&trait_ident, op))
415 .collect();
416
417 let doc = format!(" Build an axum::Router for the `{trait_ident}` trait.");
418
419 quote! {
420 #[doc = #doc]
421 pub fn #fn_ident<T>(api: T) -> ::axum::Router
422 where
423 T: #trait_ident + Clone + Send + Sync + 'static,
424 {
425 ::axum::Router::new()
426 #(#routes)*
427 .with_state(api)
428 }
429
430 #(#handlers)*
431 }
432 }
433
434 fn emit_axum_handler(&self, trait_ident: &syn::Ident, op: &OperationInfo) -> TokenStream {
435 let handler_ident = format_ident!("{}_handler", op.operation_id.to_snake_case());
436 let trait_method = format_ident!("{}", op.operation_id.to_snake_case());
437
438 let mut extractors: Vec<TokenStream> =
440 vec![quote! { ::axum::extract::State(api): ::axum::extract::State<T> }];
441 let mut call_args: Vec<TokenStream> = Vec::new();
442
443 let path_params: Vec<&_> = op
445 .parameters
446 .iter()
447 .filter(|p| p.location == "path")
448 .collect();
449 if !path_params.is_empty() {
450 let idents: Vec<syn::Ident> = path_params
451 .iter()
452 .map(|p| format_ident!("{}", p.name.to_snake_case()))
453 .collect();
454 let types: Vec<TokenStream> = path_params
455 .iter()
456 .map(|p| parse_type(&p.rust_type))
457 .collect();
458 if path_params.len() == 1 {
459 let i = &idents[0];
460 let t = &types[0];
461 extractors.push(quote! { ::axum::extract::Path(#i): ::axum::extract::Path<#t> });
462 } else {
463 extractors.push(quote! { ::axum::extract::Path((#(#idents),*)): ::axum::extract::Path<(#(#types),*)> });
464 }
465 for i in &idents {
466 call_args.push(quote! { #i });
467 }
468 }
469
470 let query_params: Vec<&_> = op
475 .parameters
476 .iter()
477 .filter(|p| p.location == "query")
478 .collect();
479 let mut required_query_checks: Vec<TokenStream> = Vec::new();
480 if !query_params.is_empty() {
481 let query_ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
482 extractors.push(quote! {
483 ::axum::extract::Query(__q): ::axum::extract::Query<#query_ident>
484 });
485 for p in &query_params {
486 let f = format_ident!("{}", p.name.to_snake_case());
487 let wire = p.name.as_str();
488 if p.required {
489 let missing_msg = format!("missing required query parameter `{wire}`");
490 required_query_checks.push(quote! {
491 let #f = match __q.#f {
492 Some(v) => v,
493 None => return ::axum::response::IntoResponse::into_response(
494 (
495 ::axum::http::StatusCode::BAD_REQUEST,
496 ::axum::Json(::serde_json::json!({
497 "error": #missing_msg
498 })),
499 )
500 ),
501 };
502 });
503 call_args.push(quote! { #f });
504 } else {
505 call_args.push(quote! { __q.#f });
506 }
507 }
508 }
509
510 let header_params: Vec<&_> = op
514 .parameters
515 .iter()
516 .filter(|p| p.location == "header")
517 .collect();
518 let mut required_header_checks: Vec<TokenStream> = Vec::new();
519 if !header_params.is_empty() {
520 extractors.push(quote! { __headers: ::axum::http::HeaderMap });
521 for p in &header_params {
522 let wire = p.name.as_str();
523 let ident = format_ident!("{}", header_param_ident(&p.name));
524 if p.required {
525 let missing_msg = format!("missing required header `{wire}`");
526 required_header_checks.push(quote! {
527 let #ident = match __headers
528 .get(#wire)
529 .and_then(|v| v.to_str().ok())
530 .map(::std::string::String::from)
531 {
532 Some(v) => v,
533 None => return ::axum::response::IntoResponse::into_response(
534 (
535 ::axum::http::StatusCode::BAD_REQUEST,
536 ::axum::Json(::serde_json::json!({
537 "error": #missing_msg
538 })),
539 )
540 ),
541 };
542 });
543 call_args.push(quote! { #ident });
544 } else {
545 call_args.push(quote! {
546 __headers
547 .get(#wire)
548 .and_then(|v| v.to_str().ok())
549 .map(::std::string::String::from)
550 });
551 }
552 }
553 }
554
555 let body_ty_opt = body_type(op);
557 if let Some(body_ty) = &body_ty_opt {
558 let body_ty_tokens = parse_type(body_ty);
559 if op.request_body_required {
560 extractors.push(quote! {
561 ::axum::Json(body): ::axum::Json<#body_ty_tokens>
562 });
563 call_args.push(quote! { body });
564 } else {
565 extractors.push(quote! {
566 body: ::std::option::Option<::axum::Json<#body_ty_tokens>>
567 });
568 call_args.push(quote! { body.map(|::axum::Json(b)| b) });
569 }
570 }
571
572 let _ = format_ident!("{}Response", op.operation_id.to_pascal_case());
573 let _ = trait_ident;
577
578 quote! {
583 async fn #handler_ident<T>(
584 #(#extractors),*
585 ) -> ::axum::response::Response
586 where
587 T: super::api::#trait_ident + Clone + Send + Sync + 'static,
588 {
589 #(#required_query_checks)*
590 #(#required_header_checks)*
591 ::axum::response::IntoResponse::into_response(
592 api.#trait_method(#(#call_args),*).await,
593 )
594 }
595 }
596 }
597
598 fn emit_api(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
599 let _ = &self.config; let traits: Vec<TokenStream> = groups
601 .iter()
602 .map(|(tag, ops)| self.emit_trait(tag, ops))
603 .collect();
604
605 let mut emitted: std::collections::BTreeSet<String> = Default::default();
611 let mut param_enums: Vec<TokenStream> = Vec::new();
612 for op in groups.values().flatten() {
613 for p in &op.parameters {
614 if let Some(values) = &p.enum_values {
615 if emitted.insert(p.rust_type.clone()) {
616 param_enums.push(emit_param_enum(&p.rust_type, values));
617 }
618 }
619 }
620 }
621
622 quote! {
623 #![allow(clippy::too_many_arguments)]
627
628 use super::errors::*;
629 #[allow(unused_imports)]
634 use super::super::types::*;
635
636 #(#param_enums)*
637
638 #(#traits)*
639 }
640 }
641
642 fn emit_trait(&self, tag: &str, ops: &[&OperationInfo]) -> TokenStream {
643 let trait_ident = trait_ident_for_tag(tag);
644 let methods: Vec<TokenStream> = ops.iter().map(|op| self.emit_method_sig(op)).collect();
645 let doc = format!(" Operations under the `{tag}` tag.");
646 quote! {
647 #[doc = #doc]
648 #[axum::async_trait]
649 pub trait #trait_ident: Send + Sync + 'static {
650 #(#methods)*
651 }
652 }
653 }
654
655 fn emit_method_sig(&self, op: &OperationInfo) -> TokenStream {
656 let name = format_ident!("{}", op.operation_id.to_snake_case());
657 let response_ty = format_ident!("{}Response", op.operation_id.to_pascal_case());
658
659 let mut params: Vec<TokenStream> = Vec::new();
664 for p in &op.parameters {
665 if p.location == "path" {
666 let ident = format_ident!("{}", p.name.to_snake_case());
667 let ty = parse_type(&p.rust_type);
668 params.push(quote! { #ident: #ty });
669 }
670 }
671 for p in &op.parameters {
672 if p.location == "query" {
673 let ident = format_ident!("{}", p.name.to_snake_case());
674 let ty = parse_type(&p.rust_type);
675 if p.required {
680 params.push(quote! { #ident: #ty });
681 } else {
682 params.push(quote! { #ident: ::std::option::Option<#ty> });
683 }
684 }
685 }
686 for p in &op.parameters {
687 if p.location == "header" {
688 let ident = format_ident!("{}", header_param_ident(&p.name));
689 if p.required {
690 params.push(quote! { #ident: String });
691 } else {
692 params.push(quote! { #ident: ::std::option::Option<String> });
693 }
694 }
695 }
696 if let Some(body) = body_type(op) {
697 let body_ty = parse_type(&body);
698 if op.request_body_required {
699 params.push(quote! { body: #body_ty });
700 } else {
701 params.push(quote! { body: Option<#body_ty> });
702 }
703 }
704
705 let summary_doc = op
706 .summary
707 .as_deref()
708 .map(|s| format!(" {s}"))
709 .unwrap_or_default();
710 let route_doc = format!(" `{} {}`", op.method, op.path);
711
712 quote! {
713 #[doc = #summary_doc]
714 #[doc = ""]
715 #[doc = #route_doc]
716 async fn #name(&self, #(#params),*) -> #response_ty;
717 }
718 }
719
720 fn emit_query_struct(&self, op: &OperationInfo) -> Option<TokenStream> {
723 let query_params: Vec<&_> = op
724 .parameters
725 .iter()
726 .filter(|p| p.location == "query")
727 .collect();
728 if query_params.is_empty() {
729 return None;
730 }
731 let ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
732 let fields: Vec<TokenStream> = query_params
733 .iter()
734 .map(|p| {
735 let f_ident = format_ident!("{}", p.name.to_snake_case());
736 let ty = parse_type(&p.rust_type);
737 let serde_rename = if p.name.to_snake_case() == p.name {
738 quote! {}
739 } else {
740 let wire = p.name.as_str();
741 quote! { #[serde(rename = #wire)] }
742 };
743 quote! {
744 #serde_rename
745 #[serde(default)]
746 pub #f_ident: ::std::option::Option<#ty>
747 }
748 })
749 .collect();
750 let doc = format!(
751 " Query parameters for `{} {}` (operationId `{}`).",
752 op.method, op.path, op.operation_id
753 );
754 Some(quote! {
755 #[doc = #doc]
756 #[derive(Debug, Default, ::serde::Deserialize)]
757 pub struct #ident {
758 #(#fields),*
759 }
760 })
761 }
762
763 fn emit_errors(&self, ops: &[&OperationInfo]) -> TokenStream {
764 let _ = &self.config; let any_streaming = ops.iter().any(|op| op.supports_streaming);
766 let enums: Vec<TokenStream> = ops.iter().map(|op| self.emit_response_enum(op)).collect();
767
768 let stream_alias = if any_streaming {
773 quote! {
774 pub type ServerEventStream = ::std::pin::Pin<
777 Box<
778 dyn ::futures_core::Stream<
779 Item = ::std::result::Result<
780 ::axum::response::sse::Event,
781 ::std::convert::Infallible,
782 >,
783 > + ::std::marker::Send
784 + 'static,
785 >,
786 >;
787
788 pub fn sse_response<S>(stream: S) -> ::axum::response::sse::Sse<ServerEventStream>
793 where
794 S: ::futures_core::Stream<
795 Item = ::std::result::Result<
796 ::axum::response::sse::Event,
797 ::std::convert::Infallible,
798 >,
799 > + ::std::marker::Send
800 + 'static,
801 {
802 ::axum::response::sse::Sse::new(Box::pin(stream))
803 }
804 }
805 } else {
806 quote! {}
807 };
808
809 quote! {
816 #![allow(clippy::large_enum_variant)]
821
822 use axum::{
823 http::StatusCode,
824 response::IntoResponse,
825 Json,
826 };
827 #[allow(unused_imports)]
832 use super::super::types::*;
833
834 #stream_alias
835
836 #(#enums)*
837 }
838 }
839
840 fn emit_response_enum(&self, op: &OperationInfo) -> TokenStream {
841 let enum_ident = format_ident!("{}Response", op.operation_id.to_pascal_case());
842 let mut variants: Vec<TokenStream> = Vec::new();
843 let mut arms: Vec<TokenStream> = Vec::new();
844
845 for (status, schema_name) in &op.response_schemas {
846 let variant = format_ident!("{}", status_variant_name(status));
847 let body_ty = parse_type(schema_name);
848 variants.push(quote! { #variant(#body_ty) });
849 let status_expr = status_token(status);
850 arms.push(quote! {
851 Self::#variant(body) => (#status_expr, Json(body)).into_response()
852 });
853 }
854
855 if op.supports_streaming {
862 variants.push(quote! {
863 OkStream(::axum::response::sse::Sse<ServerEventStream>)
864 });
865 arms.push(quote! {
866 Self::OkStream(sse) => sse.into_response()
867 });
868 }
869
870 if variants.is_empty() {
874 variants.push(quote! { Empty });
875 arms.push(quote! {
876 Self::Empty => StatusCode::NO_CONTENT.into_response()
877 });
878 }
879
880 let doc = format!(
881 " Response for `{} {}` (operationId `{}`).",
882 op.method, op.path, op.operation_id
883 );
884
885 quote! {
886 #[doc = #doc]
887 pub enum #enum_ident {
888 #(#variants),*
889 }
890
891 impl IntoResponse for #enum_ident {
892 fn into_response(self) -> ::axum::response::Response {
893 match self {
894 #(#arms),*
895 }
896 }
897 }
898 }
899 }
900}
901
902fn header_param_ident(name: &str) -> String {
906 name.replace('-', "_").to_snake_case()
907}
908
909fn emit_param_enum(name: &str, values: &[String]) -> TokenStream {
914 let enum_ident = format_ident!("{}", name);
915 let variants: Vec<TokenStream> = values
916 .iter()
917 .enumerate()
918 .map(|(i, raw)| {
919 let pascal = raw.to_pascal_case();
920 let starts_with_digit = pascal
925 .chars()
926 .next()
927 .map(|c| c.is_ascii_digit())
928 .unwrap_or(true);
929 let v_name = if pascal.is_empty() || starts_with_digit {
930 format!("Variant{i}")
931 } else {
932 pascal
933 };
934 let v_ident = format_ident!("{}", v_name);
935 let default_marker = if i == 0 {
936 quote! { #[default] }
937 } else {
938 quote! {}
939 };
940 quote! {
941 #default_marker
942 #[serde(rename = #raw)]
943 #v_ident
944 }
945 })
946 .collect();
947 quote! {
948 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Deserialize, ::serde::Serialize, Default)]
949 pub enum #enum_ident {
950 #(#variants),*
951 }
952 }
953}
954
955fn axum_method_call(method: &str) -> TokenStream {
956 match method.to_ascii_uppercase().as_str() {
957 "GET" => quote! { get },
958 "POST" => quote! { post },
959 "PUT" => quote! { put },
960 "PATCH" => quote! { patch },
961 "DELETE" => quote! { delete },
962 "HEAD" => quote! { head },
963 "OPTIONS" => quote! { options },
964 _ => quote! { any },
967 }
968}
969
970fn openapi_to_axum_path(p: &str) -> String {
975 p.to_string()
976}
977
978fn body_type(op: &OperationInfo) -> Option<String> {
979 match &op.request_body {
980 Some(RequestBodyContent::Json { schema_name })
981 | Some(RequestBodyContent::FormUrlEncoded { schema_name }) => Some(schema_name.clone()),
982 _ => None,
983 }
984}
985
986fn group_by_tag<'a>(ops: &[&'a OperationInfo]) -> BTreeMap<String, Vec<&'a OperationInfo>> {
987 let mut groups: BTreeMap<String, Vec<&OperationInfo>> = BTreeMap::new();
988 for op in ops {
989 let tag = op.tags.first().cloned().unwrap_or_else(|| "Server".into());
990 groups.entry(tag).or_default().push(op);
991 }
992 groups
993}
994
995fn trait_ident_for_tag(tag: &str) -> syn::Ident {
996 let pascal = tag.to_pascal_case();
997 let base = if pascal.is_empty() {
998 "Server".into()
999 } else {
1000 pascal
1001 };
1002 format_ident!("{}Api", base)
1003}
1004
1005fn status_variant_name(status: &str) -> String {
1008 match status {
1009 "200" => "Ok".into(),
1010 "201" => "Created".into(),
1011 "202" => "Accepted".into(),
1012 "204" => "NoContent".into(),
1013 "301" => "MovedPermanently".into(),
1014 "302" => "Found".into(),
1015 "304" => "NotModified".into(),
1016 "400" => "BadRequest".into(),
1017 "401" => "Unauthorized".into(),
1018 "403" => "Forbidden".into(),
1019 "404" => "NotFound".into(),
1020 "409" => "Conflict".into(),
1021 "410" => "Gone".into(),
1022 "422" => "UnprocessableEntity".into(),
1023 "429" => "TooManyRequests".into(),
1024 "500" => "InternalServerError".into(),
1025 "502" => "BadGateway".into(),
1026 "503" => "ServiceUnavailable".into(),
1027 "default" => "Default".into(),
1028 "2XX" => "Success".into(),
1029 "3XX" => "Redirection".into(),
1030 "4XX" => "ClientError".into(),
1031 "5XX" => "ServerError".into(),
1032 other => format!("Status{}", other.to_ascii_uppercase().replace('X', "x")),
1033 }
1034}
1035
1036fn status_token(status: &str) -> TokenStream {
1040 match status {
1041 "200" => quote! { StatusCode::OK },
1042 "201" => quote! { StatusCode::CREATED },
1043 "202" => quote! { StatusCode::ACCEPTED },
1044 "204" => quote! { StatusCode::NO_CONTENT },
1045 "301" => quote! { StatusCode::MOVED_PERMANENTLY },
1046 "302" => quote! { StatusCode::FOUND },
1047 "304" => quote! { StatusCode::NOT_MODIFIED },
1048 "400" => quote! { StatusCode::BAD_REQUEST },
1049 "401" => quote! { StatusCode::UNAUTHORIZED },
1050 "403" => quote! { StatusCode::FORBIDDEN },
1051 "404" => quote! { StatusCode::NOT_FOUND },
1052 "409" => quote! { StatusCode::CONFLICT },
1053 "410" => quote! { StatusCode::GONE },
1054 "422" => quote! { StatusCode::UNPROCESSABLE_ENTITY },
1055 "429" => quote! { StatusCode::TOO_MANY_REQUESTS },
1056 "500" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
1057 "502" => quote! { StatusCode::BAD_GATEWAY },
1058 "503" => quote! { StatusCode::SERVICE_UNAVAILABLE },
1059 "default" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
1060 "2XX" => quote! { StatusCode::OK },
1061 "3XX" => quote! { StatusCode::MOVED_PERMANENTLY },
1062 "4XX" => quote! { StatusCode::BAD_REQUEST },
1063 "5XX" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
1064 other => {
1069 if let Ok(n) = other.parse::<u16>() {
1070 quote! {
1071 StatusCode::from_u16(#n).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
1072 }
1073 } else {
1074 quote! { StatusCode::INTERNAL_SERVER_ERROR }
1075 }
1076 }
1077 }
1078}
1079
1080fn parse_type(ty: &str) -> TokenStream {
1081 syn::parse_str::<syn::Type>(ty)
1082 .map(|t| quote! { #t })
1083 .unwrap_or_else(|_| {
1084 let ident = format_ident!("{}", ty);
1085 quote! { #ident }
1086 })
1087}
1088
1089fn format_or_raw(ts: TokenStream) -> String {
1090 let raw = ts.to_string();
1091 match syn::parse_file(&raw) {
1092 Ok(parsed) => prettyplease::unparse(&parsed),
1093 Err(_) => raw,
1094 }
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099 use super::*;
1100
1101 #[test]
1102 fn status_variant_name_maps_known_codes() {
1103 assert_eq!(status_variant_name("200"), "Ok");
1104 assert_eq!(status_variant_name("4XX"), "ClientError");
1105 assert_eq!(status_variant_name("default"), "Default");
1106 assert_eq!(status_variant_name("418"), "Status418");
1107 }
1108
1109 #[test]
1110 fn trait_ident_for_tag_appends_api() {
1111 let id = trait_ident_for_tag("Responses");
1112 assert_eq!(id.to_string(), "ResponsesApi");
1113 }
1114
1115 #[test]
1116 fn untagged_falls_back_to_server_api() {
1117 let id = trait_ident_for_tag("");
1118 assert_eq!(id.to_string(), "ServerApi");
1119 }
1120}