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