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 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("cannot generate Axum route for `{path}`: {reason}")]
197 InvalidRoutePath { path: String, reason: String },
198 #[error(
199 "cannot generate Axum query extraction for `{operation_id}` parameter `{parameter}`: {reason}"
200 )]
201 UnsupportedQueryParameter {
202 operation_id: String,
203 parameter: String,
204 reason: String,
205 },
206 #[error(
207 "cannot generate unambiguous Axum query extraction for `{operation_id}`: wire key `{wire_key}` is claimed by both `{first_parameter}` and `{second_parameter}`"
208 )]
209 AmbiguousQueryParameter {
210 operation_id: String,
211 wire_key: String,
212 first_parameter: String,
213 second_parameter: String,
214 },
215 #[error("request validation: {0}")]
216 Validation(String),
217 #[error(
218 "cannot generate Axum extraction for `{operation_id}` {location} parameter `{parameter}`: only scalar schema serialization is currently supported"
219 )]
220 UnsupportedParameterSerialization {
221 operation_id: String,
222 location: String,
223 parameter: String,
224 },
225 #[error(
226 "cannot generate Axum request body for `{operation_id}` media type `{media_type}`: {reason}"
227 )]
228 UnsupportedRequestBody {
229 operation_id: String,
230 media_type: String,
231 reason: String,
232 },
233}
234
235pub struct ServerCodegen<'a> {
236 config: &'a GeneratorConfig,
237 analysis: &'a SchemaAnalysis,
238 server: &'a ServerSection,
239 source_provenance: Option<String>,
240}
241
242impl<'a> ServerCodegen<'a> {
243 pub fn new(
244 config: &'a GeneratorConfig,
245 analysis: &'a SchemaAnalysis,
246 server: &'a ServerSection,
247 ) -> Self {
248 Self {
249 config,
250 analysis,
251 server,
252 source_provenance: None,
253 }
254 }
255
256 pub fn with_source_provenance(mut self, source: Option<&str>) -> Self {
258 self.source_provenance = source.map(str::to_string);
259 self
260 }
261
262 fn provenance_attribute(&self) -> TokenStream {
263 self.source_provenance
264 .as_ref()
265 .map(|source| {
266 let provenance = format!(
267 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
268 env!("CARGO_PKG_VERSION")
269 );
270 quote! { #![doc = #provenance] }
271 })
272 .unwrap_or_default()
273 }
274
275 pub fn generate(&self) -> Result<Vec<GeneratedFile>, ServerCodegenError> {
277 if self.server.operations.is_empty() {
278 return Ok(Vec::new());
279 }
280 if !(1..=100).contains(&self.server.validation.max_errors) {
281 return Err(ServerCodegenError::Validation(
282 "max_errors must be between 1 and 100".to_string(),
283 ));
284 }
285 if !(1..=67_108_864).contains(&self.server.validation.max_body_bytes) {
286 return Err(ServerCodegenError::Validation(
287 "max_body_bytes must be between 1 and 67108864".to_string(),
288 ));
289 }
290
291 let index = OperationIndex::from_analysis(self.analysis);
292 let selectors: Vec<Selector> = self
293 .server
294 .operations
295 .iter()
296 .map(|s| Selector::parse(s))
297 .collect::<Result<_, _>>()?;
298 let resolution = super::resolve(&selectors, &index)?;
299
300 let ops: Vec<&OperationInfo> = resolution
304 .operations
305 .iter()
306 .map(|s| {
307 self.analysis
308 .operations
309 .get(&s.operation_id)
310 .ok_or_else(|| {
311 ServerCodegenError::Internal(format!(
312 "operation `{}` resolved but missing from analysis",
313 s.operation_id
314 ))
315 })
316 })
317 .collect::<Result<_, _>>()?;
318 validate_custom_method_route_groups(&ops)?;
319 self.validate_query_parameters(&ops)?;
320 self.validate_supported_server_inputs(&ops)?;
321 if !self.server.validation.enabled
322 && ops.iter().any(|operation| {
323 operation
324 .parameters
325 .iter()
326 .any(|parameter| matches!(parameter.location.as_str(), "header" | "cookie"))
327 || matches!(
328 &operation.request_body,
329 Some(RequestBodyContent::FormUrlEncoded { .. })
330 )
331 })
332 {
333 return Err(ServerCodegenError::Validation(
334 "typed header/cookie and form extraction requires server.validation.enabled=true"
335 .to_string(),
336 ));
337 }
338
339 let groups = group_by_tag(&ops);
341
342 let validation_bundle = if self.server.validation.enabled {
343 Some(
344 super::validation::prepare_validation_bundle(
345 &self.analysis.validation_context,
346 &ops,
347 )
348 .map_err(|error| ServerCodegenError::Validation(error.to_string()))?,
349 )
350 } else {
351 eprintln!(
352 "⚠️ server.validation.enabled=false: generated handlers will not enforce the OpenAPI request contract"
353 );
354 None
355 };
356
357 let api_rs = self.emit_api(&groups);
358 let errors_rs = self.emit_errors(&ops, validation_bundle.is_some());
359 let router_rs = self.emit_router(&groups, validation_bundle.as_ref())?;
360 let mod_rs = self.emit_mod(validation_bundle.is_some());
361
362 let mut files = vec![
363 GeneratedFile {
364 path: PathBuf::from("server").join("mod.rs"),
365 content: format_or_raw(mod_rs),
366 },
367 GeneratedFile {
368 path: PathBuf::from("server").join("api.rs"),
369 content: format_or_raw(api_rs),
370 },
371 GeneratedFile {
372 path: PathBuf::from("server").join("errors.rs"),
373 content: format_or_raw(errors_rs),
374 },
375 GeneratedFile {
376 path: PathBuf::from("server").join("router.rs"),
377 content: format_or_raw(router_rs),
378 },
379 ];
380 if let Some(bundle) = &validation_bundle {
381 files.push(GeneratedFile {
382 path: PathBuf::from("server").join("validation.rs"),
383 content: format_or_raw(super::validation::emit_validation_module(
384 bundle,
385 self.server.validation.max_errors,
386 )),
387 });
388 }
389 Ok(files)
390 }
391
392 fn query_parameter_type(&self, parameter: &ParameterInfo) -> TokenStream {
393 CodeGenerator::new(self.config.clone()).get_param_owned_rust_type(parameter)
394 }
395
396 fn parameter_schema_is_scalar(&self, schema: &serde_json::Value) -> bool {
397 self.parameter_schema_is_scalar_inner(schema, &mut std::collections::BTreeSet::new())
398 }
399
400 fn parameter_schema_is_string(&self, parameter: &ParameterInfo) -> bool {
401 if parameter.enum_values.is_some() || parameter.rust_type == "String" {
402 return true;
403 }
404 let Some(schema) = parameter.validation_schema.as_ref() else {
405 return false;
406 };
407 self.parameter_schema_is_string_inner(schema, &mut std::collections::BTreeSet::new())
408 }
409
410 fn parameter_schema_is_string_inner(
411 &self,
412 schema: &serde_json::Value,
413 visited: &mut std::collections::BTreeSet<String>,
414 ) -> bool {
415 if let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) {
416 if let Some(name) = reference.strip_prefix("#/components/schemas/") {
417 return self
418 .analysis
419 .validation_context
420 .component_schemas
421 .get(name)
422 .is_some_and(|component| {
423 visited.insert(name.to_string())
424 && self.parameter_schema_is_string_inner(component, visited)
425 });
426 }
427 }
428 schema.get("type").and_then(serde_json::Value::as_str) == Some("string")
429 }
430
431 fn parameter_schema_is_scalar_inner(
432 &self,
433 schema: &serde_json::Value,
434 visited: &mut std::collections::BTreeSet<String>,
435 ) -> bool {
436 if let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str)
437 && let Some(name) = reference.strip_prefix("#/components/schemas/")
438 && let Some(component) = self.analysis.validation_context.component_schemas.get(name)
439 {
440 return visited.insert(name.to_string())
441 && self.parameter_schema_is_scalar_inner(component, visited);
442 }
443 !matches!(
444 schema.get("type").and_then(serde_json::Value::as_str),
445 Some("array" | "object")
446 ) && !schema.get("oneOf").is_some()
447 && !schema.get("anyOf").is_some()
448 && !schema.get("allOf").is_some()
449 }
450
451 fn form_field_names(
452 &self,
453 operation: &OperationInfo,
454 ) -> Result<Vec<String>, ServerCodegenError> {
455 let Some(RequestBodyContent::FormUrlEncoded { schema_name, .. }) = &operation.request_body
456 else {
457 return Ok(Vec::new());
458 };
459 let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
460 ServerCodegenError::UnsupportedRequestBody {
461 operation_id: operation.operation_id.clone(),
462 media_type: "application/x-www-form-urlencoded".to_string(),
463 reason: format!("schema `{schema_name}` cannot be resolved"),
464 }
465 })?;
466 let SchemaType::Object {
467 properties,
468 additional_properties,
469 ..
470 } = &schema.schema_type
471 else {
472 return Err(ServerCodegenError::UnsupportedRequestBody {
473 operation_id: operation.operation_id.clone(),
474 media_type: "application/x-www-form-urlencoded".to_string(),
475 reason: "only flat object schemas are supported".to_string(),
476 });
477 };
478 if !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
479 || properties.values().any(|property| {
480 !self.query_property_is_scalar(
481 &property.schema_type,
482 &mut std::collections::HashSet::new(),
483 )
484 })
485 {
486 return Err(ServerCodegenError::UnsupportedRequestBody {
487 operation_id: operation.operation_id.clone(),
488 media_type: "application/x-www-form-urlencoded".to_string(),
489 reason: "only flat scalar fields with additionalProperties forbidden are supported"
490 .to_string(),
491 });
492 }
493 Ok(properties.keys().cloned().collect())
494 }
495
496 fn validate_supported_server_inputs(
497 &self,
498 operations: &[&OperationInfo],
499 ) -> Result<(), ServerCodegenError> {
500 for operation in operations {
501 for parameter in &operation.parameters {
502 if !matches!(
503 parameter.location.as_str(),
504 "path" | "query" | "header" | "cookie"
505 ) {
506 return Err(ServerCodegenError::UnsupportedParameterSerialization {
507 operation_id: operation.operation_id.clone(),
508 location: parameter.location.clone(),
509 parameter: parameter.name.clone(),
510 });
511 }
512 if matches!(parameter.location.as_str(), "path" | "header" | "cookie")
513 && parameter
514 .validation_schema
515 .as_ref()
516 .is_none_or(|schema| !self.parameter_schema_is_scalar(schema))
517 {
518 return Err(ServerCodegenError::UnsupportedParameterSerialization {
519 operation_id: operation.operation_id.clone(),
520 location: parameter.location.clone(),
521 parameter: parameter.name.clone(),
522 });
523 }
524 }
525 match &operation.request_body {
526 Some(RequestBodyContent::FormUrlEncoded { .. }) => {
527 self.form_field_names(operation)?;
528 }
529 Some(RequestBodyContent::Multipart) => {
530 return Err(ServerCodegenError::UnsupportedRequestBody {
531 operation_id: operation.operation_id.clone(),
532 media_type: "multipart/form-data".to_string(),
533 reason: "typed multipart server extraction is not implemented".to_string(),
534 });
535 }
536 Some(RequestBodyContent::OctetStream) => {
537 return Err(ServerCodegenError::UnsupportedRequestBody {
538 operation_id: operation.operation_id.clone(),
539 media_type: "application/octet-stream".to_string(),
540 reason: "binary server extraction is not implemented".to_string(),
541 });
542 }
543 Some(RequestBodyContent::TextPlain) => {
544 return Err(ServerCodegenError::UnsupportedRequestBody {
545 operation_id: operation.operation_id.clone(),
546 media_type: "text/plain".to_string(),
547 reason: "text server extraction is not implemented".to_string(),
548 });
549 }
550 Some(RequestBodyContent::SchemaLess { media_type }) => {
551 return Err(ServerCodegenError::UnsupportedRequestBody {
552 operation_id: operation.operation_id.clone(),
553 media_type: media_type.clone(),
554 reason: "request content has no schema to validate".to_string(),
555 });
556 }
557 Some(RequestBodyContent::Unsupported { media_types }) => {
558 return Err(ServerCodegenError::UnsupportedRequestBody {
559 operation_id: operation.operation_id.clone(),
560 media_type: media_types.join(", "),
561 reason: "the selected request body uses unsupported media content"
562 .to_string(),
563 });
564 }
565 _ => {}
566 }
567 }
568 Ok(())
569 }
570
571 fn parameter_ident(&self, parameter: &ParameterInfo) -> syn::Ident {
572 let generator = CodeGenerator::new(self.config.clone());
573 CodeGenerator::to_field_ident(&generator.param_ident_str(parameter))
574 }
575
576 fn validation_target(
577 &self,
578 bundle: Option<&super::validation::ValidationBundle>,
579 operation: &OperationInfo,
580 location: &str,
581 parameter_name: Option<&str>,
582 ) -> Result<Option<TokenStream>, ServerCodegenError> {
583 let Some(bundle) = bundle else {
584 return Ok(None);
585 };
586 let target = bundle
587 .target_for(&operation.operation_id, location, parameter_name)
588 .ok_or_else(|| {
589 ServerCodegenError::Validation(format!(
590 "missing generated validator target for operation `{}` {location} `{}`",
591 operation.operation_id,
592 parameter_name.unwrap_or("body")
593 ))
594 })?;
595 let ident = format_ident!("{}", target.constant);
596 Ok(Some(quote! { super::validation::#ident }))
597 }
598
599 fn resolve_query_schema(&self, schema_name: &str) -> Option<&crate::analysis::AnalyzedSchema> {
600 let mut current = schema_name;
601 let mut visited = std::collections::HashSet::new();
602 loop {
603 if !visited.insert(current) {
604 return None;
605 }
606 let schema = self.analysis.schemas.get(current)?;
607 if let SchemaType::Reference { target } = &schema.schema_type {
608 current = target;
609 } else {
610 return Some(schema);
611 }
612 }
613 }
614
615 fn query_object_properties(
616 &self,
617 parameter: &ParameterInfo,
618 ) -> Option<&BTreeMap<String, crate::analysis::PropertyInfo>> {
619 let schema = self.resolve_query_schema(parameter.schema_ref.as_deref()?)?;
620 match &schema.schema_type {
621 SchemaType::Object { properties, .. } => Some(properties),
622 _ => None,
623 }
624 }
625
626 fn query_object_required_properties(&self, parameter: &ParameterInfo) -> Vec<String> {
627 let Some(schema) = parameter
628 .schema_ref
629 .as_deref()
630 .and_then(|name| self.resolve_query_schema(name))
631 else {
632 return Vec::new();
633 };
634 let mut names = match &schema.schema_type {
635 SchemaType::Object { required, .. } => required.iter().cloned().collect(),
636 _ => Vec::new(),
637 };
638 names.sort();
639 names
640 }
641
642 fn form_required_field_names(
643 &self,
644 operation: &OperationInfo,
645 ) -> Result<Vec<String>, ServerCodegenError> {
646 let Some(RequestBodyContent::FormUrlEncoded { schema_name, .. }) = &operation.request_body
647 else {
648 return Ok(Vec::new());
649 };
650 let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
651 ServerCodegenError::UnsupportedRequestBody {
652 operation_id: operation.operation_id.clone(),
653 media_type: "application/x-www-form-urlencoded".to_string(),
654 reason: format!("schema `{schema_name}` cannot be resolved"),
655 }
656 })?;
657 let mut names = match &schema.schema_type {
658 SchemaType::Object { required, .. } => required.iter().cloned().collect(),
659 _ => Vec::new(),
660 };
661 names.sort();
662 Ok(names)
663 }
664
665 fn query_property_is_scalar(
666 &self,
667 schema_type: &SchemaType,
668 visited: &mut std::collections::HashSet<String>,
669 ) -> bool {
670 match schema_type {
671 SchemaType::Primitive { .. }
672 | SchemaType::StringEnum { .. }
673 | SchemaType::ExtensibleEnum { .. } => true,
674 SchemaType::Reference { target } if visited.insert(target.clone()) => {
675 self.analysis.schemas.get(target).is_some_and(|schema| {
676 self.query_property_is_scalar(&schema.schema_type, visited)
677 })
678 }
679 _ => false,
680 }
681 }
682
683 fn validate_query_object(
684 &self,
685 operation: &OperationInfo,
686 parameter: &ParameterInfo,
687 ) -> Result<Vec<String>, ServerCodegenError> {
688 let error = |reason: String| ServerCodegenError::UnsupportedQueryParameter {
689 operation_id: operation.operation_id.clone(),
690 parameter: parameter.name.clone(),
691 reason,
692 };
693 let schema_name = parameter.schema_ref.as_deref().ok_or_else(|| {
694 error("styled object parameter has no analyzed schema type".to_string())
695 })?;
696 let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
697 error(format!(
698 "query object schema `{schema_name}` could not be resolved"
699 ))
700 })?;
701 let (properties, additional_properties) = match &schema.schema_type {
702 SchemaType::Object {
703 properties,
704 additional_properties,
705 ..
706 } => (properties, additional_properties),
707 _ => {
708 return Err(error(format!(
709 "query schema `{schema_name}` does not resolve to a flat object"
710 )));
711 }
712 };
713 if !matches!(additional_properties, ObjectAdditionalProperties::Forbidden) {
714 return Err(error(
715 "styled object parameters with additionalProperties have an ambiguous wire namespace"
716 .to_string(),
717 ));
718 }
719 for (property_name, property) in properties {
720 if !self.query_property_is_scalar(
721 &property.schema_type,
722 &mut std::collections::HashSet::new(),
723 ) {
724 return Err(error(format!(
725 "property `{property_name}` is not scalar; nested arrays/objects are undefined for the generated query wire format"
726 )));
727 }
728 }
729 Ok(properties.keys().cloned().collect())
730 }
731
732 fn validate_query_parameters(
733 &self,
734 operations: &[&OperationInfo],
735 ) -> Result<(), ServerCodegenError> {
736 for operation in operations {
737 let mut claimed_keys: BTreeMap<String, String> = BTreeMap::new();
738 for parameter in operation
739 .parameters
740 .iter()
741 .filter(|parameter| parameter.location == "query")
742 {
743 let mut keys = match ¶meter.query_serialization {
744 Some(QuerySerialization::Unsupported { reason }) => {
745 return Err(ServerCodegenError::UnsupportedQueryParameter {
746 operation_id: operation.operation_id.clone(),
747 parameter: parameter.name.clone(),
748 reason: reason.clone(),
749 });
750 }
751 Some(
752 QuerySerialization::FormExplodedObject
753 | QuerySerialization::FormObject
754 | QuerySerialization::DeepObject,
755 ) => {
756 let property_keys = self.validate_query_object(operation, parameter)?;
757 if matches!(
758 parameter.query_serialization,
759 Some(QuerySerialization::FormExplodedObject)
760 ) {
761 property_keys
762 } else if matches!(
763 parameter.query_serialization,
764 Some(QuerySerialization::DeepObject)
765 ) {
766 property_keys
767 .into_iter()
768 .map(|property| format!("{}[{property}]", parameter.name))
769 .collect()
770 } else {
771 vec![parameter.name.clone()]
772 }
773 }
774 Some(
775 QuerySerialization::FormExplodedArray { .. }
776 | QuerySerialization::FormArray { .. },
777 )
778 | None => vec![parameter.name.clone()],
779 };
780 if matches!(
781 ¶meter.query_serialization,
782 Some(
783 QuerySerialization::FormExplodedObject
784 | QuerySerialization::FormObject
785 | QuerySerialization::DeepObject
786 | QuerySerialization::FormExplodedArray { .. }
787 | QuerySerialization::FormArray { .. }
788 )
789 ) {
790 keys.push(format!("{}[]", parameter.name));
791 }
792 for key in keys {
793 if let Some(first_parameter) =
794 claimed_keys.insert(key.clone(), parameter.name.clone())
795 {
796 return Err(ServerCodegenError::AmbiguousQueryParameter {
797 operation_id: operation.operation_id.clone(),
798 wire_key: key,
799 first_parameter,
800 second_parameter: parameter.name.clone(),
801 });
802 }
803 }
804 }
805 }
806 Ok(())
807 }
808
809 fn emit_mod(&self, validation_enabled: bool) -> TokenStream {
810 let provenance_attribute = self.provenance_attribute();
811 let validation_module = validation_enabled.then(|| quote! { pub(crate) mod validation; });
812 quote! {
813 #provenance_attribute
819
820 pub mod api;
821 pub mod errors;
822 pub mod router;
823 #validation_module
824
825 pub use api::*;
826 pub use errors::*;
827 pub use router::*;
828 }
829 }
830
831 fn emit_router(
832 &self,
833 groups: &BTreeMap<String, Vec<&OperationInfo>>,
834 validation_bundle: Option<&super::validation::ValidationBundle>,
835 ) -> Result<TokenStream, ServerCodegenError> {
836 let provenance_attribute = self.provenance_attribute();
837 let factories: Vec<TokenStream> = groups
838 .iter()
839 .map(|(tag, ops)| self.emit_router_for_trait(tag, ops, validation_bundle))
840 .collect::<Result<_, _>>()?;
841
842 let query_structs: Vec<TokenStream> = groups
844 .values()
845 .flatten()
846 .filter_map(|op| self.emit_query_struct(op))
847 .collect();
848 let has_query_parameters = groups.values().flatten().any(|operation| {
849 operation
850 .parameters
851 .iter()
852 .any(|parameter| parameter.location == "query")
853 });
854 let query_helpers = has_query_parameters.then(|| {
855 quote! {
856 fn __query_pairs(raw: ::std::option::Option<&str>) -> ::std::vec::Vec<(String, String)> {
857 raw.map(|query| {
858 ::url::form_urlencoded::parse(query.as_bytes())
859 .into_owned()
860 .collect()
861 })
862 .unwrap_or_default()
863 }
864
865 fn __validate_urlencoded(raw: &str) -> ::std::result::Result<(), String> {
866 let bytes = raw.as_bytes();
867 let mut index = 0;
868 while index < bytes.len() {
869 if bytes[index] == b'%' {
870 if index + 2 >= bytes.len()
871 || !bytes[index + 1].is_ascii_hexdigit()
872 || !bytes[index + 2].is_ascii_hexdigit()
873 {
874 return Err("malformed percent encoding".to_string());
875 }
876 index += 3;
877 } else {
878 index += 1;
879 }
880 }
881 Ok(())
882 }
883
884 fn __query_one(
885 pairs: &[(String, String)],
886 key: &str,
887 ) -> ::std::result::Result<::std::option::Option<String>, String> {
888 let mut values = pairs
889 .iter()
890 .filter(|(candidate, _)| candidate == key)
891 .map(|(_, value)| value.clone());
892 let value = values.next();
893 if values.next().is_some() {
894 return Err(format!("query parameter `{key}` appeared more than once"));
895 }
896 Ok(value)
897 }
898
899 fn __decode_query_scalar<T>(
900 value: &str,
901 label: &str,
902 ) -> ::std::result::Result<T, String>
903 where
904 T: ::serde::de::DeserializeOwned,
905 {
906 ::serde_json::from_value(::serde_json::Value::String(value.to_string()))
907 .or_else(|_| ::serde_json::from_str(value))
908 .map_err(|error| format!("invalid query value for `{label}`: {error}"))
909 }
910
911 fn __decode_query_object<T>(
912 fields: &[(String, String)],
913 label: &str,
914 ) -> ::std::result::Result<T, String>
915 where
916 T: ::serde::de::DeserializeOwned,
917 {
918 let mut serializer =
919 ::url::form_urlencoded::Serializer::new(String::new());
920 for (key, value) in fields {
921 serializer.append_pair(key, value);
922 }
923 ::serde_urlencoded::from_str(&serializer.finish())
924 .map_err(|error| format!("invalid query object `{label}`: {error}"))
925 }
926
927 fn __query_empty_marker(
928 pairs: &[(String, String)],
929 key: &str,
930 ) -> ::std::result::Result<bool, String> {
931 let marker = format!("{key}[]");
932 match __query_one(pairs, &marker)? {
933 Some(value) if value.is_empty() => Ok(true),
934 Some(_) => Err(format!(
935 "zero-cardinality marker `{marker}` must have an empty value"
936 )),
937 None => Ok(false),
938 }
939 }
940 }
941 });
942
943 let combined = if groups.len() > 1 {
949 Some(self.emit_combined_router(groups))
950 } else {
951 None
952 };
953
954 Ok(quote! {
955 #provenance_attribute
960
961 use super::api::*;
962 use super::errors::*;
963 #[allow(unused_imports)]
968 use super::super::types::*;
969
970 #query_helpers
971
972 #(#query_structs)*
973
974 #(#factories)*
975
976 #combined
977 })
978 }
979
980 fn emit_combined_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
981 let entries: Vec<(syn::Ident, syn::Ident, syn::Ident)> = groups
985 .keys()
986 .enumerate()
987 .map(|(i, tag)| {
988 let trait_ident = trait_ident_for_tag(tag);
989 let factory = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
990 let generic = format_ident!("T{}", i + 1);
991 (trait_ident, factory, generic)
992 })
993 .collect();
994
995 let generics: Vec<&syn::Ident> = entries.iter().map(|(_, _, g)| g).collect();
996 let args: Vec<TokenStream> = entries
997 .iter()
998 .map(|(trait_ident, _, g)| {
999 let arg_ident = format_ident!("{}", trait_ident.to_string().to_snake_case());
1000 quote! { #arg_ident: #g }
1001 })
1002 .collect();
1003 let bounds: Vec<TokenStream> = entries
1004 .iter()
1005 .map(|(trait_ident, _, g)| {
1006 quote! { #g: #trait_ident + Clone + Send + Sync + 'static }
1007 })
1008 .collect();
1009
1010 let first = &entries[0];
1012 let first_arg = format_ident!("{}", first.0.to_string().to_snake_case());
1013 let first_factory = &first.1;
1014 let rest = entries
1015 .iter()
1016 .skip(1)
1017 .map(|(trait_ident, factory, _)| {
1018 let arg = format_ident!("{}", trait_ident.to_string().to_snake_case());
1019 quote! { .merge(#factory(#arg)) }
1020 })
1021 .collect::<Vec<_>>();
1022
1023 let trait_names: Vec<String> = entries.iter().map(|(t, _, _)| t.to_string()).collect();
1024 let doc = format!(
1025 " Combined router spanning {} traits: {}.",
1026 entries.len(),
1027 trait_names.join(", "),
1028 );
1029
1030 quote! {
1031 #[doc = #doc]
1032 pub fn build_router<#(#generics),*>(
1033 #(#args),*
1034 ) -> ::axum::Router
1035 where
1036 #(#bounds),*
1037 {
1038 #first_factory(#first_arg) #(#rest)*
1039 }
1040 }
1041 }
1042
1043 fn emit_router_for_trait(
1044 &self,
1045 tag: &str,
1046 ops: &[&OperationInfo],
1047 validation_bundle: Option<&super::validation::ValidationBundle>,
1048 ) -> Result<TokenStream, ServerCodegenError> {
1049 let trait_ident = trait_ident_for_tag(tag);
1050 let fn_ident = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
1051
1052 let mut routes: Vec<TokenStream> = Vec::new();
1053 let mut custom_by_path: BTreeMap<String, Vec<(String, syn::Ident)>> = BTreeMap::new();
1054 for op in ops {
1055 let handler = format_ident!("{}_handler", op.operation_id.to_snake_case());
1056 let path = openapi_to_axum_path(&op.path)?;
1057 if let Some(method_call) = axum_method_call(&op.method) {
1058 routes.push(quote! { .route(#path, ::axum::routing::#method_call(#handler::<T>)) });
1059 } else {
1060 custom_by_path
1061 .entry(path)
1062 .or_default()
1063 .push((op.method.to_ascii_uppercase(), handler));
1064 }
1065 }
1066 let mut custom_dispatchers = Vec::new();
1067 for (path, methods) in custom_by_path {
1068 let first_handler = &methods[0].1;
1069 let dispatcher = format_ident!("{}_custom_method_dispatch", first_handler);
1070 let (route, dispatcher_fn) =
1071 axum_custom_route(&path, &dispatcher, &methods, &trait_ident);
1072 routes.push(route);
1073 custom_dispatchers.push(dispatcher_fn);
1074 }
1075
1076 let handlers: Vec<TokenStream> = ops
1077 .iter()
1078 .map(|op| self.emit_axum_handler(&trait_ident, op, validation_bundle))
1079 .collect::<Result<_, _>>()?;
1080
1081 let doc = format!(" Build an axum::Router for the `{trait_ident}` trait.");
1082
1083 Ok(quote! {
1084 #[doc = #doc]
1085 pub fn #fn_ident<T>(api: T) -> ::axum::Router
1086 where
1087 T: #trait_ident + Clone + Send + Sync + 'static,
1088 {
1089 ::axum::Router::new()
1090 #(#routes)*
1091 .with_state(api)
1092 }
1093
1094 #(#custom_dispatchers)*
1095
1096 #(#handlers)*
1097 })
1098 }
1099
1100 fn emit_axum_handler(
1101 &self,
1102 trait_ident: &syn::Ident,
1103 op: &OperationInfo,
1104 validation_bundle: Option<&super::validation::ValidationBundle>,
1105 ) -> Result<TokenStream, ServerCodegenError> {
1106 let handler_ident = format_ident!("{}_handler", op.operation_id.to_snake_case());
1107 let trait_method = format_ident!("{}", op.operation_id.to_snake_case());
1108
1109 let mut extractors: Vec<TokenStream> =
1111 vec![quote! { ::axum::extract::State(api): ::axum::extract::State<T> }];
1112 let mut call_args: Vec<TokenStream> = Vec::new();
1113
1114 let path_params: Vec<&_> = op
1118 .parameters
1119 .iter()
1120 .filter(|p| p.location == "path")
1121 .collect();
1122 let mut path_decode = TokenStream::new();
1123 if !path_params.is_empty() && validation_bundle.is_some() {
1124 extractors.push(quote! {
1125 __path_result: ::std::result::Result<
1126 ::axum::extract::Path<::std::collections::HashMap<String, String>>,
1127 ::axum::extract::rejection::PathRejection,
1128 >
1129 });
1130 let mut decoders = Vec::new();
1131 for parameter in &path_params {
1132 let ident = self.parameter_ident(parameter);
1133 let ty = self.query_parameter_type(parameter);
1134 let wire = parameter.name.as_str();
1135 let location = parameter_location("path", wire);
1136 let target = self
1137 .validation_target(validation_bundle, op, "path", Some(wire))?
1138 .ok_or_else(|| {
1139 ServerCodegenError::Validation(format!(
1140 "validation target unexpectedly disabled for operation `{}` path `{wire}`",
1141 op.operation_id
1142 ))
1143 })?;
1144 let string_wire = self.parameter_schema_is_string(parameter);
1145 decoders.push(quote! {
1146 let #ident: #ty = match __path_values.remove(#wire) {
1147 Some(raw) => match super::validation::decode_parameter(
1148 &raw, #target, #location, #string_wire,
1149 ) {
1150 Ok(value) => value,
1151 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1152 },
1153 None => return ::axum::response::IntoResponse::into_response(
1154 super::validation::generated_contract_error()
1155 ),
1156 };
1157 });
1158 call_args.push(quote! { #ident });
1159 }
1160 path_decode = quote! {
1161 let ::axum::extract::Path(mut __path_values) = match __path_result {
1162 Ok(path) => path,
1163 Err(_) => return ::axum::response::IntoResponse::into_response(
1164 super::validation::malformed_parameter("/path")
1165 ),
1166 };
1167 #(#decoders)*
1168 };
1169 } else if !path_params.is_empty() {
1170 let idents: Vec<syn::Ident> = path_params
1171 .iter()
1172 .map(|p| self.parameter_ident(p))
1173 .collect();
1174 let types: Vec<TokenStream> = path_params
1175 .iter()
1176 .map(|p| self.query_parameter_type(p))
1177 .collect();
1178 if path_params.len() == 1 {
1179 let i = &idents[0];
1180 let t = &types[0];
1181 extractors.push(quote! { ::axum::extract::Path(#i): ::axum::extract::Path<#t> });
1182 } else {
1183 extractors.push(quote! { ::axum::extract::Path((#(#idents),*)): ::axum::extract::Path<(#(#types),*)> });
1184 }
1185 for i in &idents {
1186 call_args.push(quote! { #i });
1187 }
1188 }
1189
1190 let query_params: Vec<&_> = op
1195 .parameters
1196 .iter()
1197 .filter(|p| p.location == "query")
1198 .collect();
1199 let mut required_query_checks: Vec<TokenStream> = Vec::new();
1200 let mut query_validation_checks: Vec<TokenStream> = Vec::new();
1201 let mut raw_query_validation_checks: Vec<TokenStream> = Vec::new();
1202 let mut query_decode = TokenStream::new();
1203 if !query_params.is_empty() {
1204 let query_ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
1205 let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
1206 extractors.push(quote! {
1207 ::axum::extract::RawQuery(__raw_query): ::axum::extract::RawQuery
1208 });
1209 query_decode = if validation_bundle.is_some() {
1210 quote! {
1211 let __q: #query_ident = match #decode_ident(__raw_query.as_deref()) {
1212 Ok(query) => query,
1213 Err(_) => return ::axum::response::IntoResponse::into_response(
1214 super::validation::malformed_parameter("/query")
1215 ),
1216 };
1217 }
1218 } else {
1219 quote! {
1220 let __q: #query_ident = match #decode_ident(__raw_query.as_deref()) {
1221 Ok(query) => query,
1222 Err(message) => return ::axum::response::IntoResponse::into_response(
1223 (
1224 ::axum::http::StatusCode::BAD_REQUEST,
1225 ::axum::Json(::serde_json::json!({ "error": message })),
1226 )
1227 ),
1228 };
1229 }
1230 };
1231 for p in &query_params {
1232 let f = self.parameter_ident(p);
1233 let wire = p.name.as_str();
1234 let location = parameter_location("query", wire);
1235 let target = self.validation_target(validation_bundle, op, "query", Some(wire))?;
1236 if p.query_serialization.is_none() && self.parameter_schema_is_string(p) {
1237 if let Some(target) = target.as_ref() {
1238 raw_query_validation_checks.push(quote! {
1239 if let Ok(Some(raw)) = __query_one(&__raw_query_pairs, #wire) {
1240 if let Err(rejection) = super::validation::validate_string_parameter(
1241 #target, #location, &raw,
1242 ) {
1243 return ::axum::response::IntoResponse::into_response(rejection);
1244 }
1245 }
1246 });
1247 }
1248 }
1249 if p.required {
1250 required_query_checks.push(if validation_bundle.is_some() {
1251 quote! {
1252 let #f = match __q.#f {
1253 Some(v) => v,
1254 None => return ::axum::response::IntoResponse::into_response(
1255 super::validation::missing_parameter(#location)
1256 ),
1257 };
1258 }
1259 } else {
1260 let missing_msg = format!("missing required query parameter `{wire}`");
1261 quote! {
1262 let #f = match __q.#f {
1263 Some(v) => v,
1264 None => return ::axum::response::IntoResponse::into_response(
1265 (
1266 ::axum::http::StatusCode::BAD_REQUEST,
1267 ::axum::Json(::serde_json::json!({
1268 "error": #missing_msg
1269 })),
1270 )
1271 ),
1272 };
1273 }
1274 });
1275 if let Some(target) = target {
1276 query_validation_checks.push(quote! {
1277 if let Err(rejection) = super::validation::validate_parameter(
1278 #target, #location, &#f,
1279 ) {
1280 return ::axum::response::IntoResponse::into_response(rejection);
1281 }
1282 });
1283 }
1284 call_args.push(quote! { #f });
1285 } else {
1286 if let Some(target) = target {
1287 query_validation_checks.push(quote! {
1288 if let Some(value) = &__q.#f {
1289 if let Err(rejection) = super::validation::validate_parameter(
1290 #target, #location, value,
1291 ) {
1292 return ::axum::response::IntoResponse::into_response(rejection);
1293 }
1294 }
1295 });
1296 }
1297 call_args.push(quote! { __q.#f });
1298 }
1299 }
1300 }
1301
1302 let header_params: Vec<&_> = op
1306 .parameters
1307 .iter()
1308 .filter(|p| p.location == "header")
1309 .collect();
1310 let cookie_params: Vec<&_> = op
1311 .parameters
1312 .iter()
1313 .filter(|p| p.location == "cookie")
1314 .collect();
1315 let mut parameter_decode_checks: Vec<TokenStream> = Vec::new();
1316 if !header_params.is_empty() || !cookie_params.is_empty() {
1317 extractors.push(quote! { __headers: ::axum::http::HeaderMap });
1318 }
1319 if !header_params.is_empty() {
1320 for p in &header_params {
1321 let wire = p.name.as_str();
1322 let ident = self.parameter_ident(p);
1323 let ty = self.query_parameter_type(p);
1324 let location = parameter_location("header", wire);
1325 let target = self.validation_target(validation_bundle, op, "header", Some(wire))?;
1326 let string_wire = self.parameter_schema_is_string(p);
1327 if p.required {
1328 if let Some(target) = target {
1329 parameter_decode_checks.push(quote! {
1330 let mut __values = __headers.get_all(#wire).iter();
1331 let #ident: #ty = match (__values.next(), __values.next()) {
1332 (Some(value), None) => match value.to_str() {
1333 Ok(raw) => match super::validation::decode_parameter(
1334 raw, #target, #location, #string_wire,
1335 ) {
1336 Ok(value) => value,
1337 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1338 },
1339 Err(_) => return ::axum::response::IntoResponse::into_response(
1340 super::validation::malformed_parameter(#location)
1341 ),
1342 },
1343 (None, _) => return ::axum::response::IntoResponse::into_response(
1344 super::validation::missing_parameter(#location)
1345 ),
1346 _ => return ::axum::response::IntoResponse::into_response(
1347 super::validation::malformed_parameter(#location)
1348 ),
1349 };
1350 });
1351 } else {
1352 parameter_decode_checks.push(quote! {
1353 let #ident: #ty = match __headers.get(#wire).and_then(|v| v.to_str().ok()) {
1354 Some(raw) => match __decode_query_scalar(raw, #wire) {
1355 Ok(value) => value,
1356 Err(_) => return ::axum::http::StatusCode::BAD_REQUEST.into_response(),
1357 },
1358 None => return ::axum::http::StatusCode::BAD_REQUEST.into_response(),
1359 };
1360 });
1361 }
1362 call_args.push(quote! { #ident });
1363 } else {
1364 if let Some(target) = target {
1365 parameter_decode_checks.push(quote! {
1366 let mut __values = __headers.get_all(#wire).iter();
1367 let #ident: ::std::option::Option<#ty> = match (__values.next(), __values.next()) {
1368 (Some(value), None) => match value.to_str() {
1369 Ok(raw) => match super::validation::decode_parameter(raw, #target, #location, #string_wire) {
1370 Ok(value) => Some(value),
1371 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1372 },
1373 Err(_) => return ::axum::response::IntoResponse::into_response(
1374 super::validation::malformed_parameter(#location)
1375 ),
1376 },
1377 (None, _) => None,
1378 _ => return ::axum::response::IntoResponse::into_response(
1379 super::validation::malformed_parameter(#location)
1380 ),
1381 };
1382 });
1383 } else {
1384 parameter_decode_checks.push(quote! {
1385 let #ident: ::std::option::Option<#ty> = __headers.get(#wire)
1386 .and_then(|value| value.to_str().ok())
1387 .and_then(|raw| __decode_query_scalar(raw, #wire).ok());
1388 });
1389 }
1390 call_args.push(quote! { #ident });
1391 }
1392 }
1393 }
1394 if !cookie_params.is_empty() {
1395 parameter_decode_checks.push(quote! {
1396 let mut __cookies = match super::validation::parse_cookies(&__headers) {
1397 Ok(cookies) => cookies,
1398 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1399 };
1400 });
1401 for p in &cookie_params {
1402 let wire = p.name.as_str();
1403 let ident = self.parameter_ident(p);
1404 let ty = self.query_parameter_type(p);
1405 let location = parameter_location("cookie", wire);
1406 let target = self
1407 .validation_target(validation_bundle, op, "cookie", Some(wire))?
1408 .ok_or_else(|| {
1409 ServerCodegenError::Validation(format!(
1410 "cookie extraction requires validation for operation `{}`",
1411 op.operation_id
1412 ))
1413 })?;
1414 let string_wire = self.parameter_schema_is_string(p);
1415 if p.required {
1416 parameter_decode_checks.push(quote! {
1417 let #ident: #ty = match __cookies.remove(#wire) {
1418 Some(raw) => match super::validation::decode_parameter(&raw, #target, #location, #string_wire) {
1419 Ok(value) => value,
1420 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1421 },
1422 None => return ::axum::response::IntoResponse::into_response(
1423 super::validation::missing_parameter(#location)
1424 ),
1425 };
1426 });
1427 call_args.push(quote! { #ident });
1428 } else {
1429 parameter_decode_checks.push(quote! {
1430 let #ident: ::std::option::Option<#ty> = match __cookies.remove(#wire) {
1431 Some(raw) => match super::validation::decode_parameter(&raw, #target, #location, #string_wire) {
1432 Ok(value) => Some(value),
1433 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1434 },
1435 None => None,
1436 };
1437 });
1438 call_args.push(quote! { #ident });
1439 }
1440 }
1441 }
1442
1443 let mut body_decode = TokenStream::new();
1445 let body_ty_opt = body_type(op);
1446 if let Some(body_ty) = &body_ty_opt {
1447 let body_ty_tokens = parse_type(body_ty);
1448 let validated_json = matches!(&op.request_body, Some(RequestBodyContent::Json { .. }))
1449 && validation_bundle.is_some();
1450 let validated_form = matches!(
1451 &op.request_body,
1452 Some(RequestBodyContent::FormUrlEncoded { .. })
1453 ) && validation_bundle.is_some();
1454 if validated_json {
1455 extractors.push(quote! { __request: ::axum::extract::Request });
1456 let Some(RequestBodyContent::Json { media_type, .. }) = &op.request_body else {
1457 return Err(ServerCodegenError::Internal(
1458 "validated JSON body lost its media type".to_string(),
1459 ));
1460 };
1461 let target = self
1462 .validation_target(validation_bundle, op, "body", None)?
1463 .ok_or_else(|| {
1464 ServerCodegenError::Validation(format!(
1465 "validation target unexpectedly disabled for operation `{}` body",
1466 op.operation_id
1467 ))
1468 })?;
1469 let required = op.request_body_required;
1470 let max_body_bytes = self.server.validation.max_body_bytes;
1471 body_decode = if required {
1472 quote! {
1473 let body: #body_ty_tokens = match super::validation::decode_json_body::<#body_ty_tokens>(
1474 __request,
1475 #target,
1476 #media_type,
1477 true,
1478 #max_body_bytes,
1479 ).await {
1480 Ok(Some(body)) => body,
1481 Ok(None) => return ::axum::response::IntoResponse::into_response(
1482 super::validation::generated_contract_error()
1483 ),
1484 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1485 };
1486 }
1487 } else {
1488 quote! {
1489 let body: ::std::option::Option<#body_ty_tokens> =
1490 match super::validation::decode_json_body::<#body_ty_tokens>(
1491 __request,
1492 #target,
1493 #media_type,
1494 false,
1495 #max_body_bytes,
1496 ).await {
1497 Ok(body) => body,
1498 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1499 };
1500 }
1501 };
1502 call_args.push(quote! { body });
1503 } else if validated_form {
1504 extractors.push(quote! { __request: ::axum::extract::Request });
1505 let Some(RequestBodyContent::FormUrlEncoded { media_type, .. }) = &op.request_body
1506 else {
1507 return Err(ServerCodegenError::Internal(
1508 "validated form body lost its media type".to_string(),
1509 ));
1510 };
1511 let target = self
1512 .validation_target(validation_bundle, op, "body", None)?
1513 .ok_or_else(|| {
1514 ServerCodegenError::Validation(format!(
1515 "validation target unexpectedly disabled for operation `{}` body",
1516 op.operation_id
1517 ))
1518 })?;
1519 let allowed_fields = self.form_field_names(op)?;
1520 let required_fields = self.form_required_field_names(op)?;
1521 let required = op.request_body_required;
1522 let max_body_bytes = self.server.validation.max_body_bytes;
1523 body_decode = quote! {
1524 let body: ::std::option::Option<#body_ty_tokens> =
1525 match super::validation::decode_form_body::<#body_ty_tokens>(
1526 __request,
1527 #target,
1528 #media_type,
1529 #required,
1530 #max_body_bytes,
1531 &[#(#allowed_fields),*],
1532 &[#(#required_fields),*],
1533 ).await {
1534 Ok(body) => body,
1535 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1536 };
1537 };
1538 if required {
1539 body_decode.extend(quote! {
1540 let body = match body {
1541 Some(body) => body,
1542 None => return ::axum::response::IntoResponse::into_response(
1543 super::validation::generated_contract_error()
1544 ),
1545 };
1546 });
1547 }
1548 call_args.push(quote! { body });
1549 } else if op.request_body_required {
1550 extractors.push(quote! {
1551 ::axum::Json(body): ::axum::Json<#body_ty_tokens>
1552 });
1553 call_args.push(quote! { body });
1554 } else {
1555 extractors.push(quote! {
1556 body: ::std::option::Option<::axum::Json<#body_ty_tokens>>
1557 });
1558 call_args.push(quote! { body.map(|::axum::Json(b)| b) });
1559 }
1560 }
1561
1562 let _ = format_ident!("{}Response", op.operation_id.to_pascal_case());
1563 let _ = trait_ident;
1567 let raw_query_validation = (!raw_query_validation_checks.is_empty()).then(|| {
1568 quote! {
1569 if let Some(raw) = __raw_query.as_deref() {
1570 if __validate_urlencoded(raw).is_err() {
1571 return ::axum::response::IntoResponse::into_response(
1572 super::validation::malformed_parameter("/query")
1573 );
1574 }
1575 }
1576 let __raw_query_pairs = __query_pairs(__raw_query.as_deref());
1577 #(#raw_query_validation_checks)*
1578 }
1579 });
1580
1581 Ok(quote! {
1586 async fn #handler_ident<T>(
1587 #(#extractors),*
1588 ) -> ::axum::response::Response
1589 where
1590 T: super::api::#trait_ident + Clone + Send + Sync + 'static,
1591 {
1592 #path_decode
1593 #raw_query_validation
1594 #query_decode
1595 #(#required_query_checks)*
1596 #(#query_validation_checks)*
1597 #(#parameter_decode_checks)*
1598 #body_decode
1599 ::axum::response::IntoResponse::into_response(
1600 api.#trait_method(#(#call_args),*).await,
1601 )
1602 }
1603 })
1604 }
1605
1606 fn emit_api(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
1607 let provenance_attribute = self.provenance_attribute();
1608 let traits: Vec<TokenStream> = groups
1609 .iter()
1610 .map(|(tag, ops)| self.emit_trait(tag, ops))
1611 .collect();
1612
1613 let mut emitted: std::collections::BTreeSet<String> = Default::default();
1619 let mut param_enums: Vec<TokenStream> = Vec::new();
1620 for op in groups.values().flatten() {
1621 for p in &op.parameters {
1622 if let Some(values) = &p.enum_values {
1623 if emitted.insert(p.rust_type.clone()) {
1624 param_enums.push(emit_param_enum(&p.rust_type, values));
1625 }
1626 }
1627 }
1628 }
1629
1630 quote! {
1631 #provenance_attribute
1635
1636 #![allow(clippy::too_many_arguments)]
1637
1638 use super::errors::*;
1639 #[allow(unused_imports)]
1644 use super::super::types::*;
1645
1646 #(#param_enums)*
1647
1648 #(#traits)*
1649 }
1650 }
1651
1652 fn emit_trait(&self, tag: &str, ops: &[&OperationInfo]) -> TokenStream {
1653 let trait_ident = trait_ident_for_tag(tag);
1654 let methods: Vec<TokenStream> = ops.iter().map(|op| self.emit_method_sig(op)).collect();
1655 let doc = format!(" Operations under the `{tag}` tag.");
1656 quote! {
1657 #[doc = #doc]
1658 #[async_trait::async_trait]
1659 pub trait #trait_ident: Send + Sync + 'static {
1660 #(#methods)*
1661 }
1662 }
1663 }
1664
1665 fn emit_method_sig(&self, op: &OperationInfo) -> TokenStream {
1666 let name = format_ident!("{}", op.operation_id.to_snake_case());
1667 let response_ty = format_ident!("{}Response", op.operation_id.to_pascal_case());
1668
1669 let mut params: Vec<TokenStream> = Vec::new();
1674 for p in &op.parameters {
1675 if p.location == "path" {
1676 let ident = self.parameter_ident(p);
1677 let ty = self.query_parameter_type(p);
1678 params.push(quote! { #ident: #ty });
1679 }
1680 }
1681 for p in &op.parameters {
1682 if p.location == "query" {
1683 let ident = self.parameter_ident(p);
1684 let ty = self.query_parameter_type(p);
1685 if p.required {
1690 params.push(quote! { #ident: #ty });
1691 } else {
1692 params.push(quote! { #ident: ::std::option::Option<#ty> });
1693 }
1694 }
1695 }
1696 for p in &op.parameters {
1697 if p.location == "header" {
1698 let ident = self.parameter_ident(p);
1699 let ty = self.query_parameter_type(p);
1700 if p.required {
1701 params.push(quote! { #ident: #ty });
1702 } else {
1703 params.push(quote! { #ident: ::std::option::Option<#ty> });
1704 }
1705 }
1706 }
1707 for p in &op.parameters {
1708 if p.location == "cookie" {
1709 let ident = self.parameter_ident(p);
1710 let ty = self.query_parameter_type(p);
1711 if p.required {
1712 params.push(quote! { #ident: #ty });
1713 } else {
1714 params.push(quote! { #ident: ::std::option::Option<#ty> });
1715 }
1716 }
1717 }
1718 if let Some(body) = body_type(op) {
1719 let body_ty = parse_type(&body);
1720 if op.request_body_required {
1721 params.push(quote! { body: #body_ty });
1722 } else {
1723 params.push(quote! { body: Option<#body_ty> });
1724 }
1725 }
1726
1727 let summary_doc = op
1728 .summary
1729 .as_deref()
1730 .map(|s| format!(" {s}"))
1731 .unwrap_or_default();
1732 let route_doc = format!(" `{} {}`", op.method, op.path);
1733
1734 quote! {
1735 #[doc = #summary_doc]
1736 #[doc = ""]
1737 #[doc = #route_doc]
1738 async fn #name(&self, #(#params),*) -> #response_ty;
1739 }
1740 }
1741
1742 fn emit_query_struct(&self, op: &OperationInfo) -> Option<TokenStream> {
1746 let query_params: Vec<&_> = op
1747 .parameters
1748 .iter()
1749 .filter(|p| p.location == "query")
1750 .collect();
1751 if query_params.is_empty() {
1752 return None;
1753 }
1754 let ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
1755 let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
1756 let mut fields = Vec::new();
1757 let mut decoders = Vec::new();
1758 let mut field_idents = Vec::new();
1759 for parameter in query_params {
1760 let field_ident = self.parameter_ident(parameter);
1761 let field_type = self.query_parameter_type(parameter);
1762 let wire_name = parameter.name.as_str();
1763 fields.push(quote! {
1764 pub #field_ident: ::std::option::Option<#field_type>
1765 });
1766 field_idents.push(field_ident.clone());
1767
1768 let decoder = match ¶meter.query_serialization {
1769 Some(QuerySerialization::FormExplodedArray { .. }) => quote! {
1770 let #field_ident = {
1771 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
1772 let raw_values: Vec<&str> = __pairs
1773 .iter()
1774 .filter(|(key, _)| key == #wire_name)
1775 .map(|(_, value)| value.as_str())
1776 .collect();
1777 if empty_marker && !raw_values.is_empty() {
1778 return Err(format!(
1779 "query array `{}` cannot combine values with its empty marker",
1780 #wire_name,
1781 ));
1782 }
1783 if empty_marker {
1784 Some(Vec::new())
1785 } else if raw_values.is_empty() {
1786 None
1787 } else {
1788 let mut values = Vec::with_capacity(raw_values.len());
1789 for raw in raw_values {
1790 values.push(__decode_query_scalar(raw, #wire_name)?);
1791 }
1792 Some(values)
1793 }
1794 };
1795 },
1796 Some(QuerySerialization::FormArray { .. }) => quote! {
1797 let #field_ident = match (
1798 __query_one(&__pairs, #wire_name)?,
1799 __query_empty_marker(&__pairs, #wire_name)?,
1800 ) {
1801 (Some(_), true) => return Err(format!(
1802 "query array `{}` cannot combine a value with its empty marker",
1803 #wire_name,
1804 )),
1805 (Some(raw), false) => {
1806 let mut values = Vec::new();
1807 for item in raw.split(',') {
1808 values.push(__decode_query_scalar(item, #wire_name)?);
1809 }
1810 Some(values)
1811 }
1812 (None, true) => Some(Vec::new()),
1813 (None, false) => None,
1814 };
1815 },
1816 Some(QuerySerialization::FormExplodedObject) => {
1817 let property_names = self
1818 .query_object_properties(parameter)
1819 .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
1820 .unwrap_or_default();
1821 let required_names = self.query_object_required_properties(parameter);
1822 quote! {
1823 let #field_ident = {
1824 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
1825 let allowed = [#(#property_names),*];
1826 let object_fields: Vec<(String, String)> = __pairs
1827 .iter()
1828 .filter(|(key, _)| allowed.contains(&key.as_str()))
1829 .cloned()
1830 .collect();
1831 if empty_marker && !object_fields.is_empty() {
1832 return Err(format!(
1833 "query object `{}` cannot combine properties with its empty marker",
1834 #wire_name,
1835 ));
1836 }
1837 if object_fields.is_empty() && !empty_marker {
1838 None
1839 } else {
1840 let required_properties: &[&str] = &[#(#required_names),*];
1841 for required in required_properties {
1842 if !object_fields.iter().any(|(key, _)| key == required) {
1843 return Err(format!("query object `{}` is missing a required property", #wire_name));
1844 }
1845 }
1846 Some(__decode_query_object(&object_fields, #wire_name)?)
1847 }
1848 };
1849 }
1850 }
1851 Some(QuerySerialization::FormObject) => {
1852 let property_names = self
1853 .query_object_properties(parameter)
1854 .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
1855 .unwrap_or_default();
1856 let required_names = self.query_object_required_properties(parameter);
1857 let has_required_names = !required_names.is_empty();
1858 quote! {
1859 let #field_ident = match (
1860 __query_one(&__pairs, #wire_name)?,
1861 __query_empty_marker(&__pairs, #wire_name)?,
1862 ) {
1863 (Some(_), true) => return Err(format!(
1864 "query object `{}` cannot combine a value with its empty marker",
1865 #wire_name,
1866 )),
1867 (Some(raw), false) => {
1868 let parts: Vec<&str> = raw.split(',').collect();
1869 if parts.len() % 2 != 0 {
1870 return Err(format!(
1871 "query object `{}` must contain alternating key,value entries",
1872 #wire_name,
1873 ));
1874 }
1875 let allowed = [#(#property_names),*];
1876 let mut seen = ::std::collections::BTreeSet::new();
1877 let object_fields: Vec<(String, String)> = parts
1878 .chunks_exact(2)
1879 .map(|pair| (pair[0].to_string(), pair[1].to_string()))
1880 .collect();
1881 for (key, _) in &object_fields {
1882 if !allowed.contains(&key.as_str()) || !seen.insert(key.as_str()) {
1883 return Err(format!("query object `{}` has invalid properties", #wire_name));
1884 }
1885 }
1886 let required_properties: &[&str] = &[#(#required_names),*];
1887 for required in required_properties {
1888 if !seen.contains(required) {
1889 return Err(format!("query object `{}` is missing a required property", #wire_name));
1890 }
1891 }
1892 Some(__decode_query_object(&object_fields, #wire_name)?)
1893 }
1894 (None, true) if #has_required_names => return Err(format!(
1895 "query object `{}` is missing a required property",
1896 #wire_name,
1897 )),
1898 (None, true) => Some(__decode_query_object(&[], #wire_name)?),
1899 (None, false) => None,
1900 };
1901 }
1902 }
1903 Some(QuerySerialization::DeepObject) => {
1904 let property_names = self
1905 .query_object_properties(parameter)
1906 .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
1907 .unwrap_or_default();
1908 let required_names = self.query_object_required_properties(parameter);
1909 quote! {
1910 let #field_ident = {
1911 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
1912 let prefix = format!("{}[", #wire_name);
1913 let allowed = [#(#property_names),*];
1914 let mut object_fields = Vec::new();
1915 for (key, value) in &__pairs {
1916 if let Some(property) = key
1917 .strip_prefix(&prefix)
1918 .and_then(|rest| rest.strip_suffix(']'))
1919 {
1920 if property.is_empty() {
1921 continue;
1922 }
1923 if !allowed.contains(&property) {
1924 return Err(format!(
1925 "unknown deepObject property `{}[{}]`",
1926 #wire_name,
1927 property,
1928 ));
1929 }
1930 object_fields.push((property.to_string(), value.clone()));
1931 }
1932 }
1933 if empty_marker && !object_fields.is_empty() {
1934 return Err(format!(
1935 "query object `{}` cannot combine properties with its empty marker",
1936 #wire_name,
1937 ));
1938 }
1939 if object_fields.is_empty() && !empty_marker {
1940 None
1941 } else {
1942 let required_properties: &[&str] = &[#(#required_names),*];
1943 for required in required_properties {
1944 if !object_fields.iter().any(|(key, _)| key == required) {
1945 return Err(format!("query object `{}` is missing a required property", #wire_name));
1946 }
1947 }
1948 Some(__decode_query_object(&object_fields, #wire_name)?)
1949 }
1950 };
1951 }
1952 }
1953 Some(QuerySerialization::Unsupported { .. }) | None => quote! {
1954 let #field_ident = __query_one(&__pairs, #wire_name)?
1955 .map(|raw| __decode_query_scalar(&raw, #wire_name))
1956 .transpose()?;
1957 },
1958 };
1959 decoders.push(decoder);
1960 }
1961 let doc = format!(
1962 " Query parameters for `{} {}` (operationId `{}`).",
1963 op.method, op.path, op.operation_id
1964 );
1965 Some(quote! {
1966 #[doc = #doc]
1967 #[derive(Debug, Default)]
1968 pub struct #ident {
1969 #(#fields),*
1970 }
1971
1972 fn #decode_ident(
1973 raw: ::std::option::Option<&str>,
1974 ) -> ::std::result::Result<#ident, String> {
1975 if let Some(raw) = raw {
1976 __validate_urlencoded(raw)?;
1977 }
1978 let __pairs = __query_pairs(raw);
1979 #(#decoders)*
1980 Ok(#ident {
1981 #(#field_idents),*
1982 })
1983 }
1984 })
1985 }
1986
1987 fn emit_errors(&self, ops: &[&OperationInfo], validation_enabled: bool) -> TokenStream {
1988 let provenance_attribute = self.provenance_attribute();
1989 let any_streaming = ops.iter().any(|op| op.supports_streaming);
1990 let enums: Vec<TokenStream> = ops.iter().map(|op| self.emit_response_enum(op)).collect();
1991 let problem_types = validation_enabled.then(|| {
1992 quote! {
1993 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)]
1995 pub struct ProblemDetails {
1996 #[serde(rename = "type")]
1997 pub r#type: String,
1998 pub title: String,
1999 pub status: u16,
2000 pub code: String,
2001 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2002 pub errors: Vec<InvalidParameter>,
2003 }
2004
2005 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)]
2007 pub struct InvalidParameter {
2008 pub code: String,
2009 pub location: String,
2010 pub message: String,
2011 }
2012
2013 #[derive(Debug, Clone)]
2015 pub struct RequestValidationRejection(pub ProblemDetails);
2016
2017 impl IntoResponse for RequestValidationRejection {
2018 fn into_response(self) -> ::axum::response::Response {
2019 let status = StatusCode::from_u16(self.0.status)
2020 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
2021 let mut response = (status, Json(self.0)).into_response();
2022 response.headers_mut().insert(
2023 ::axum::http::header::CONTENT_TYPE,
2024 ::axum::http::HeaderValue::from_static("application/problem+json"),
2025 );
2026 response
2027 }
2028 }
2029 }
2030 });
2031
2032 let stream_alias = if any_streaming {
2037 quote! {
2038 pub type ServerEventStream = ::std::pin::Pin<
2041 Box<
2042 dyn ::futures_core::Stream<
2043 Item = ::std::result::Result<
2044 ::axum::response::sse::Event,
2045 ::std::convert::Infallible,
2046 >,
2047 > + ::std::marker::Send
2048 + 'static,
2049 >,
2050 >;
2051
2052 pub fn sse_response<S>(stream: S) -> ::axum::response::sse::Sse<ServerEventStream>
2057 where
2058 S: ::futures_core::Stream<
2059 Item = ::std::result::Result<
2060 ::axum::response::sse::Event,
2061 ::std::convert::Infallible,
2062 >,
2063 > + ::std::marker::Send
2064 + 'static,
2065 {
2066 ::axum::response::sse::Sse::new(Box::pin(stream))
2067 }
2068 }
2069 } else {
2070 quote! {}
2071 };
2072
2073 quote! {
2080 #provenance_attribute
2085
2086 #![allow(clippy::large_enum_variant)]
2087
2088 use axum::{
2089 http::StatusCode,
2090 response::IntoResponse,
2091 Json,
2092 };
2093 #[allow(unused_imports)]
2098 use super::super::types::*;
2099
2100 #problem_types
2101
2102 #stream_alias
2103
2104 #(#enums)*
2105 }
2106 }
2107
2108 fn emit_response_enum(&self, op: &OperationInfo) -> TokenStream {
2109 let enum_ident = format_ident!("{}Response", op.operation_id.to_pascal_case());
2110 let mut variants: Vec<TokenStream> = Vec::new();
2111 let mut arms: Vec<TokenStream> = Vec::new();
2112
2113 for (status, schema_name) in &op.response_schemas {
2114 let variant = format_ident!("{}", status_variant_name(status));
2115 let body_ty = parse_type(schema_name);
2116 variants.push(quote! { #variant(#body_ty) });
2117 let status_expr = status_token(status);
2118 arms.push(quote! {
2119 Self::#variant(body) => (#status_expr, Json(body)).into_response()
2120 });
2121 }
2122
2123 if op.supports_streaming {
2130 variants.push(quote! {
2131 OkStream(::axum::response::sse::Sse<ServerEventStream>)
2132 });
2133 arms.push(quote! {
2134 Self::OkStream(sse) => sse.into_response()
2135 });
2136 }
2137
2138 if variants.is_empty() {
2142 variants.push(quote! { Empty });
2143 arms.push(quote! {
2144 Self::Empty => StatusCode::NO_CONTENT.into_response()
2145 });
2146 }
2147
2148 let doc = format!(
2149 " Response for `{} {}` (operationId `{}`).",
2150 op.method, op.path, op.operation_id
2151 );
2152
2153 quote! {
2154 #[doc = #doc]
2155 pub enum #enum_ident {
2156 #(#variants),*
2157 }
2158
2159 impl IntoResponse for #enum_ident {
2160 fn into_response(self) -> ::axum::response::Response {
2161 match self {
2162 #(#arms),*
2163 }
2164 }
2165 }
2166 }
2167 }
2168}
2169
2170fn parameter_location(location: &str, name: &str) -> String {
2171 format!("/{location}/{}", name.replace('~', "~0").replace('/', "~1"))
2172}
2173
2174fn emit_param_enum(name: &str, values: &[String]) -> TokenStream {
2179 let enum_ident = format_ident!("{}", name);
2180 let variants: Vec<TokenStream> = values
2181 .iter()
2182 .enumerate()
2183 .map(|(i, raw)| {
2184 let pascal = raw.to_pascal_case();
2185 let starts_with_digit = pascal
2190 .chars()
2191 .next()
2192 .map(|c| c.is_ascii_digit())
2193 .unwrap_or(true);
2194 let v_name = if pascal.is_empty() || starts_with_digit {
2195 format!("Variant{i}")
2196 } else {
2197 pascal
2198 };
2199 let v_ident = format_ident!("{}", v_name);
2200 let default_marker = if i == 0 {
2201 quote! { #[default] }
2202 } else {
2203 quote! {}
2204 };
2205 quote! {
2206 #default_marker
2207 #[serde(rename = #raw)]
2208 #v_ident
2209 }
2210 })
2211 .collect();
2212 quote! {
2213 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Deserialize, ::serde::Serialize, Default)]
2214 pub enum #enum_ident {
2215 #(#variants),*
2216 }
2217 }
2218}
2219
2220fn axum_method_call(method: &str) -> Option<TokenStream> {
2221 match method.to_ascii_uppercase().as_str() {
2222 "CONNECT" => Some(quote! { connect }),
2223 "DELETE" => Some(quote! { delete }),
2224 "GET" => Some(quote! { get }),
2225 "HEAD" => Some(quote! { head }),
2226 "OPTIONS" => Some(quote! { options }),
2227 "PATCH" => Some(quote! { patch }),
2228 "POST" => Some(quote! { post }),
2229 "PUT" => Some(quote! { put }),
2230 "TRACE" => Some(quote! { trace }),
2231 _ => None,
2232 }
2233}
2234
2235fn axum_custom_route(
2242 path: &str,
2243 dispatcher: &syn::Ident,
2244 methods: &[(String, syn::Ident)],
2245 trait_ident: &syn::Ident,
2246) -> (TokenStream, TokenStream) {
2247 let arms = methods.iter().map(|(method, handler)| {
2248 quote! {
2249 #method => ::axum::handler::Handler::call(#handler::<T>, request, api).await
2250 }
2251 });
2252 let route = quote! {
2253 .route(#path, ::axum::routing::any(#dispatcher::<T>))
2254 };
2255 let dispatcher_fn = quote! {
2256 async fn #dispatcher<T>(
2257 ::axum::extract::State(api): ::axum::extract::State<T>,
2258 request: ::axum::extract::Request,
2259 ) -> ::axum::response::Response
2260 where
2261 T: #trait_ident + Clone + Send + Sync + 'static,
2262 {
2263 match request.method().as_str() {
2264 #(#arms,)*
2265 _ => ::axum::response::IntoResponse::into_response(
2266 ::axum::http::StatusCode::METHOD_NOT_ALLOWED,
2267 ),
2268 }
2269 }
2270 };
2271 (route, dispatcher_fn)
2272}
2273
2274fn openapi_to_axum_path(path: &str) -> Result<String, ServerCodegenError> {
2281 let invalid = |reason: &str| ServerCodegenError::InvalidRoutePath {
2282 path: path.to_string(),
2283 reason: reason.to_string(),
2284 };
2285
2286 if !path.starts_with('/') {
2287 return Err(invalid("paths must start with `/`"));
2288 }
2289 for segment in path.split('/').skip(1) {
2290 if segment.is_empty() {
2291 continue;
2292 }
2293 if segment.starts_with(':') || segment.starts_with('*') {
2294 return Err(invalid(
2295 "segments beginning with `:` or `*` conflict with Axum route syntax",
2296 ));
2297 }
2298
2299 let has_open = segment.contains('{');
2300 let has_close = segment.contains('}');
2301 if has_open || has_close {
2302 let Some(name) = segment
2303 .strip_prefix('{')
2304 .and_then(|value| value.strip_suffix('}'))
2305 else {
2306 return Err(invalid(
2307 "path parameters must occupy a complete segment such as `{pet_id}`",
2308 ));
2309 };
2310 if name.is_empty() || name.contains(['{', '}']) {
2311 return Err(invalid(
2312 "path parameter names must be non-empty and cannot contain braces",
2313 ));
2314 }
2315 }
2316 }
2317
2318 Ok(path.to_string())
2319}
2320
2321fn body_type(op: &OperationInfo) -> Option<String> {
2322 match &op.request_body {
2323 Some(RequestBodyContent::Json { schema_name, .. })
2324 | Some(RequestBodyContent::FormUrlEncoded { schema_name, .. }) => Some(schema_name.clone()),
2325 _ => None,
2326 }
2327}
2328
2329fn group_by_tag<'a>(ops: &[&'a OperationInfo]) -> BTreeMap<String, Vec<&'a OperationInfo>> {
2330 let mut groups: BTreeMap<String, Vec<&OperationInfo>> = BTreeMap::new();
2331 for op in ops {
2332 let tag = primary_tag(op);
2333 groups.entry(tag).or_default().push(op);
2334 }
2335 groups
2336}
2337
2338fn primary_tag(op: &OperationInfo) -> String {
2339 op.tags.first().cloned().unwrap_or_else(|| "Server".into())
2340}
2341
2342fn validate_custom_method_route_groups(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
2343 let mut tags_by_path: BTreeMap<&str, std::collections::BTreeSet<String>> = BTreeMap::new();
2344 for op in ops {
2345 if axum_method_call(&op.method).is_none() {
2346 tags_by_path
2347 .entry(&op.path)
2348 .or_default()
2349 .insert(primary_tag(op));
2350 }
2351 }
2352 if let Some((path, tags)) = tags_by_path.into_iter().find(|(_, tags)| tags.len() > 1) {
2353 return Err(ServerCodegenError::CrossTagCustomMethods {
2354 path: path.to_string(),
2355 tags: tags.into_iter().collect::<Vec<_>>().join(", "),
2356 });
2357 }
2358 Ok(())
2359}
2360
2361fn trait_ident_for_tag(tag: &str) -> syn::Ident {
2362 let pascal = tag.to_pascal_case();
2363 let base = if pascal.is_empty() {
2364 "Server".into()
2365 } else {
2366 pascal
2367 };
2368 format_ident!("{}Api", base)
2369}
2370
2371fn status_variant_name(status: &str) -> String {
2374 match status {
2375 "200" => "Ok".into(),
2376 "201" => "Created".into(),
2377 "202" => "Accepted".into(),
2378 "204" => "NoContent".into(),
2379 "301" => "MovedPermanently".into(),
2380 "302" => "Found".into(),
2381 "304" => "NotModified".into(),
2382 "400" => "BadRequest".into(),
2383 "401" => "Unauthorized".into(),
2384 "403" => "Forbidden".into(),
2385 "404" => "NotFound".into(),
2386 "409" => "Conflict".into(),
2387 "410" => "Gone".into(),
2388 "422" => "UnprocessableEntity".into(),
2389 "429" => "TooManyRequests".into(),
2390 "500" => "InternalServerError".into(),
2391 "502" => "BadGateway".into(),
2392 "503" => "ServiceUnavailable".into(),
2393 "default" => "Default".into(),
2394 "2XX" => "Success".into(),
2395 "3XX" => "Redirection".into(),
2396 "4XX" => "ClientError".into(),
2397 "5XX" => "ServerError".into(),
2398 other => format!("Status{}", other.to_ascii_uppercase().replace('X', "x")),
2399 }
2400}
2401
2402fn status_token(status: &str) -> TokenStream {
2406 match status {
2407 "200" => quote! { StatusCode::OK },
2408 "201" => quote! { StatusCode::CREATED },
2409 "202" => quote! { StatusCode::ACCEPTED },
2410 "204" => quote! { StatusCode::NO_CONTENT },
2411 "301" => quote! { StatusCode::MOVED_PERMANENTLY },
2412 "302" => quote! { StatusCode::FOUND },
2413 "304" => quote! { StatusCode::NOT_MODIFIED },
2414 "400" => quote! { StatusCode::BAD_REQUEST },
2415 "401" => quote! { StatusCode::UNAUTHORIZED },
2416 "403" => quote! { StatusCode::FORBIDDEN },
2417 "404" => quote! { StatusCode::NOT_FOUND },
2418 "409" => quote! { StatusCode::CONFLICT },
2419 "410" => quote! { StatusCode::GONE },
2420 "422" => quote! { StatusCode::UNPROCESSABLE_ENTITY },
2421 "429" => quote! { StatusCode::TOO_MANY_REQUESTS },
2422 "500" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
2423 "502" => quote! { StatusCode::BAD_GATEWAY },
2424 "503" => quote! { StatusCode::SERVICE_UNAVAILABLE },
2425 "default" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
2426 "2XX" => quote! { StatusCode::OK },
2427 "3XX" => quote! { StatusCode::MOVED_PERMANENTLY },
2428 "4XX" => quote! { StatusCode::BAD_REQUEST },
2429 "5XX" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
2430 other => {
2435 if let Ok(n) = other.parse::<u16>() {
2436 quote! {
2437 StatusCode::from_u16(#n).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
2438 }
2439 } else {
2440 quote! { StatusCode::INTERNAL_SERVER_ERROR }
2441 }
2442 }
2443 }
2444}
2445
2446fn parse_type(ty: &str) -> TokenStream {
2447 syn::parse_str::<syn::Type>(ty)
2448 .map(|t| quote! { #t })
2449 .unwrap_or_else(|_| {
2450 let ident = format_ident!("{}", ty);
2451 quote! { #ident }
2452 })
2453}
2454
2455fn format_or_raw(ts: TokenStream) -> String {
2456 let raw = ts.to_string();
2457 match syn::parse_file(&raw) {
2458 Ok(parsed) => prettyplease::unparse(&parsed),
2459 Err(_) => raw,
2460 }
2461}
2462
2463#[cfg(test)]
2464mod tests {
2465 use super::*;
2466
2467 #[test]
2468 fn status_variant_name_maps_known_codes() {
2469 assert_eq!(status_variant_name("200"), "Ok");
2470 assert_eq!(status_variant_name("4XX"), "ClientError");
2471 assert_eq!(status_variant_name("default"), "Default");
2472 assert_eq!(status_variant_name("418"), "Status418");
2473 }
2474
2475 #[test]
2476 fn trait_ident_for_tag_appends_api() {
2477 let id = trait_ident_for_tag("Responses");
2478 assert_eq!(id.to_string(), "ResponsesApi");
2479 }
2480
2481 #[test]
2482 fn untagged_falls_back_to_server_api() {
2483 let id = trait_ident_for_tag("");
2484 assert_eq!(id.to_string(), "ServerApi");
2485 }
2486
2487 #[test]
2488 fn custom_methods_on_one_path_share_an_exact_dispatcher() {
2489 let dispatcher = format_ident!("cache_custom_method_dispatch");
2490 let methods = vec![
2491 ("PURGE".to_string(), format_ident!("purge_cache_handler")),
2492 ("QUERY".to_string(), format_ident!("query_cache_handler")),
2493 ];
2494 let trait_ident = format_ident!("CacheApi");
2495 let (route, dispatcher) = axum_custom_route("/cache", &dispatcher, &methods, &trait_ident);
2496 let route = route.to_string();
2497 let dispatcher = dispatcher.to_string();
2498 assert!(route.contains("routing :: any"));
2499 assert_eq!(route.matches("routing :: any").count(), 1);
2500 assert!(dispatcher.contains("\"PURGE\""));
2501 assert!(dispatcher.contains("\"QUERY\""));
2502 assert!(dispatcher.contains("purge_cache_handler"));
2503 assert!(dispatcher.contains("query_cache_handler"));
2504 assert!(dispatcher.contains("METHOD_NOT_ALLOWED"));
2505 }
2506
2507 #[test]
2508 fn standard_methods_use_axum_method_routes_without_guards() {
2509 assert!(axum_method_call("TRACE").is_some());
2510 assert!(axum_method_call("QUERY").is_none());
2511 }
2512
2513 #[test]
2514 fn openapi_parameterized_paths_are_axum_08_paths() {
2515 assert_eq!(
2516 openapi_to_axum_path("/pets/{pet_id}").unwrap(),
2517 "/pets/{pet_id}"
2518 );
2519 assert_eq!(openapi_to_axum_path("/").unwrap(), "/");
2520 }
2521
2522 #[test]
2523 fn malformed_or_unsupported_route_templates_are_rejected() {
2524 for path in [
2525 "pets/{pet_id}",
2526 "/pets/{pet_id}.json",
2527 "/pets/{}",
2528 "/pets/:id",
2529 ] {
2530 assert!(
2531 matches!(
2532 openapi_to_axum_path(path),
2533 Err(ServerCodegenError::InvalidRoutePath { .. })
2534 ),
2535 "{path} should be rejected"
2536 );
2537 }
2538 }
2539}