1use crate::analysis::{
11 ObjectAdditionalProperties, OperationInfo, OperationResponse, OperationResponseBody,
12 ParameterInfo, QuerySerialization, RequestBodyContent, SchemaAnalysis, SchemaType,
13};
14use crate::config::ServerSection;
15use crate::generator::{CodeGenerator, GeneratedFile, GeneratorConfig, rust_type_name};
16
17use super::{OperationIndex, Selector};
18use heck::{ToPascalCase, ToSnakeCase};
19use proc_macro2::TokenStream;
20use quote::{format_ident, quote};
21use std::collections::BTreeMap;
22use std::path::PathBuf;
23
24#[derive(Clone, Copy)]
25enum MultipartFieldKind {
26 Binary,
27 String,
28 Integer,
29 UnsignedInteger,
30 Number,
31 Boolean,
32}
33
34struct MultipartFieldPlan {
35 wire_name: String,
36 field_ident: syn::Ident,
37 required: bool,
38 kind: MultipartFieldKind,
39}
40
41pub fn reachable_schemas(
56 analysis: &SchemaAnalysis,
57 ops: &[&OperationInfo],
58) -> std::collections::BTreeSet<String> {
59 reachable_schemas_with_roots(analysis, ops, &[])
60}
61
62pub fn reachable_schemas_with_roots(
65 analysis: &SchemaAnalysis,
66 ops: &[&OperationInfo],
67 extra_roots: &[String],
68) -> std::collections::BTreeSet<String> {
69 let mut keep: std::collections::BTreeSet<String> = Default::default();
70 let mut queue: Vec<String> = Vec::new();
71
72 let seed =
73 |name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
74 if !name.is_empty() && keep.insert(name.to_string()) {
75 queue.push(name.to_string());
76 }
77 };
78
79 for op in ops {
80 if let Some(rb) = &op.request_body
81 && let Some(name) = rb.schema_name()
82 {
83 seed(name, &mut queue, &mut keep);
84 }
85 for ty in op.response_schemas.values() {
86 seed(ty, &mut queue, &mut keep);
87 }
88 if let Some(responses) = analysis.operation_responses.get(&op.operation_id) {
91 for response in responses.values() {
92 for representation in response.representations.values() {
93 if let crate::analysis::OperationRepresentation::Json { schema_name } =
94 representation
95 {
96 seed(schema_name, &mut queue, &mut keep);
97 }
98 }
99 }
100 }
101 for p in &op.parameters {
102 if let Some(name) = &p.schema_ref {
103 seed(name, &mut queue, &mut keep);
104 }
105 if let Some(
106 QuerySerialization::FormExplodedArray {
107 item_type: crate::analysis::ArrayItemType::SchemaRef(name),
108 }
109 | QuerySerialization::FormArray {
110 item_type: crate::analysis::ArrayItemType::SchemaRef(name),
111 }
112 | QuerySerialization::SimpleHeaderArray {
113 item_type: crate::analysis::ArrayItemType::SchemaRef(name),
114 },
115 ) = &p.query_serialization
116 {
117 seed(name, &mut queue, &mut keep);
118 }
119 if let Some(
120 QuerySerialization::FormExplodedArray {
121 item_type:
122 crate::analysis::ArrayItemType::FlatStructRef { schema_name, .. }
123 | crate::analysis::ArrayItemType::NestedStructRef { schema_name, .. },
124 }
125 | QuerySerialization::FormArray {
126 item_type:
127 crate::analysis::ArrayItemType::FlatStructRef { schema_name, .. }
128 | crate::analysis::ArrayItemType::NestedStructRef { schema_name, .. },
129 }
130 | QuerySerialization::SimpleHeaderArray {
131 item_type:
132 crate::analysis::ArrayItemType::FlatStructRef { schema_name, .. }
133 | crate::analysis::ArrayItemType::NestedStructRef { schema_name, .. },
134 },
135 ) = &p.query_serialization
136 {
137 seed(schema_name, &mut queue, &mut keep);
138 }
139 }
140 }
141 for root in extra_roots {
142 seed(root, &mut queue, &mut keep);
143 }
144
145 while let Some(name) = queue.pop() {
146 if let Some(schema) = analysis.schemas.get(&name) {
147 collect_refs(&schema.original, &mut queue, &mut keep);
150 for dep in &schema.dependencies {
155 seed(dep, &mut queue, &mut keep);
156 }
157 collect_schema_type_refs(&schema.schema_type, &mut queue, &mut keep);
161 }
162 }
163
164 keep
165}
166
167fn collect_schema_type_refs(
168 schema_type: &SchemaType,
169 queue: &mut Vec<String>,
170 keep: &mut std::collections::BTreeSet<String>,
171) {
172 let seed =
173 |name: &str, queue: &mut Vec<String>, keep: &mut std::collections::BTreeSet<String>| {
174 if !name.is_empty() && keep.insert(name.to_string()) {
175 queue.push(name.to_string());
176 }
177 };
178
179 match schema_type {
180 SchemaType::Primitive { .. }
181 | SchemaType::StringEnum { .. }
182 | SchemaType::ExtensibleEnum { .. } => {}
183 SchemaType::Object {
184 properties,
185 additional_properties,
186 ..
187 } => {
188 for property in properties.values() {
189 collect_schema_type_refs(&property.schema_type, queue, keep);
190 }
191 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
192 collect_schema_type_refs(value_type, queue, keep);
193 }
194 }
195 SchemaType::DiscriminatedUnion { variants, .. } => {
196 for variant in variants {
197 seed(&variant.type_name, queue, keep);
198 }
199 }
200 SchemaType::Union { variants, .. } | SchemaType::Composition { schemas: variants } => {
201 for variant in variants {
202 seed(&variant.target, queue, keep);
203 }
204 }
205 SchemaType::Array { item_type } => collect_schema_type_refs(item_type, queue, keep),
206 SchemaType::Nullable { inner_type } => collect_schema_type_refs(inner_type, queue, keep),
207 SchemaType::Tuple { element_types } => {
208 for element_type in element_types {
209 collect_schema_type_refs(element_type, queue, keep);
210 }
211 }
212 SchemaType::Reference { target } => seed(target, queue, keep),
213 SchemaType::Untyped { .. } => {}
214 }
215}
216
217fn collect_refs(
218 value: &serde_json::Value,
219 queue: &mut Vec<String>,
220 keep: &mut std::collections::BTreeSet<String>,
221) {
222 match value {
223 serde_json::Value::Object(map) => {
224 for (k, v) in map {
225 if k == "$ref"
226 && let Some(s) = v.as_str()
227 && let Some(name) = s.strip_prefix("#/components/schemas/")
228 && keep.insert(name.to_string())
229 {
230 queue.push(name.to_string());
231 }
232 collect_refs(v, queue, keep);
233 }
234 }
235 serde_json::Value::Array(items) => {
236 for v in items {
237 collect_refs(v, queue, keep);
238 }
239 }
240 _ => {}
241 }
242}
243
244#[derive(Debug, thiserror::Error)]
245pub enum ServerCodegenError {
246 #[error("server selector: {0}")]
247 Parse(#[from] super::SelectorParseError),
248 #[error("server selector: {0}")]
249 Resolve(#[from] super::SelectorResolveError),
250 #[error("internal: {0}")]
251 Internal(String),
252 #[error(
253 "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"
254 )]
255 CrossTagCustomMethods { path: String, tags: String },
256 #[error(
257 "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"
258 )]
259 TagIdentifierCollision {
260 first_tag: String,
261 second_tag: String,
262 identifier: String,
263 },
264 #[error("cannot generate Axum route for `{path}`: {reason}")]
265 InvalidRoutePath { path: String, reason: String },
266 #[error(
267 "cannot generate Axum query extraction for `{operation_id}` parameter `{parameter}`: {reason}"
268 )]
269 UnsupportedQueryParameter {
270 operation_id: String,
271 parameter: String,
272 reason: String,
273 },
274 #[error(
275 "cannot generate unambiguous Axum query extraction for `{operation_id}`: wire key `{wire_key}` is claimed by both `{first_parameter}` and `{second_parameter}`"
276 )]
277 AmbiguousQueryParameter {
278 operation_id: String,
279 wire_key: String,
280 first_parameter: String,
281 second_parameter: String,
282 },
283 #[error("request validation: {0}")]
284 Validation(String),
285 #[error(
286 "cannot generate Axum extraction for `{operation_id}` {location} parameter `{parameter}`: only scalar schema serialization is currently supported"
287 )]
288 UnsupportedParameterSerialization {
289 operation_id: String,
290 location: String,
291 parameter: String,
292 },
293 #[error(
294 "cannot generate Axum request body for `{operation_id}` media type `{media_type}`: {reason}"
295 )]
296 UnsupportedRequestBody {
297 operation_id: String,
298 media_type: String,
299 reason: String,
300 },
301 #[error(
302 "cannot generate response for `{operation_id}` status `{status}`: unsupported media content {media_types}"
303 )]
304 UnsupportedResponseContent {
305 operation_id: String,
306 status: String,
307 media_types: String,
308 },
309}
310
311pub struct ServerCodegen<'a> {
312 config: &'a GeneratorConfig,
313 analysis: &'a SchemaAnalysis,
314 server: &'a ServerSection,
315 source_provenance: Option<String>,
316}
317
318impl<'a> ServerCodegen<'a> {
319 fn resolve_multipart_schema<'b>(
320 &'b self,
321 schema: &'b serde_json::Value,
322 ) -> Result<&'b serde_json::Value, ServerCodegenError> {
323 self.resolve_multipart_schema_inner(schema, &mut std::collections::HashSet::new())
324 }
325
326 fn resolve_multipart_schema_inner<'b>(
327 &'b self,
328 schema: &'b serde_json::Value,
329 visited: &mut std::collections::HashSet<String>,
330 ) -> Result<&'b serde_json::Value, ServerCodegenError> {
331 let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) else {
332 return Ok(schema);
333 };
334 let Some(name) = reference.strip_prefix("#/components/schemas/") else {
335 return Ok(schema);
336 };
337 if !visited.insert(name.to_string()) {
338 return Err(ServerCodegenError::Validation(format!(
339 "multipart schema reference cycle includes `{name}`"
340 )));
341 }
342 let resolved = self
343 .analysis
344 .validation_context
345 .component_schemas
346 .get(name)
347 .ok_or_else(|| {
348 ServerCodegenError::Validation(format!(
349 "multipart schema reference `{reference}` could not be resolved"
350 ))
351 })?;
352 self.resolve_multipart_schema_inner(resolved, visited)
353 }
354
355 fn multipart_field_plans(
356 &self,
357 schema: &serde_json::Value,
358 ) -> Result<Vec<MultipartFieldPlan>, ServerCodegenError> {
359 let schema = self.resolve_multipart_schema(schema)?;
360 let properties = schema
361 .get("properties")
362 .and_then(serde_json::Value::as_object)
363 .ok_or_else(|| {
364 ServerCodegenError::Validation(
365 "multipart request schema must be a flat object with declared properties"
366 .into(),
367 )
368 })?;
369 if schema
370 .get("additionalProperties")
371 .is_some_and(|value| value != &serde_json::Value::Bool(false))
372 {
373 return Err(ServerCodegenError::Validation(
374 "multipart request schema cannot use additionalProperties".into(),
375 ));
376 }
377 let required = schema
378 .get("required")
379 .and_then(serde_json::Value::as_array)
380 .into_iter()
381 .flatten()
382 .filter_map(serde_json::Value::as_str)
383 .collect::<std::collections::HashSet<_>>();
384 properties
385 .iter()
386 .map(|(wire_name, property)| {
387 let property = self.resolve_multipart_schema(property)?;
388 let kind = match (
389 property.get("type").and_then(serde_json::Value::as_str),
390 property.get("format").and_then(serde_json::Value::as_str),
391 ) {
392 (Some("string"), Some("binary")) => match self.config.types.binary {
393 crate::type_mapping::BinaryStrategy::String => MultipartFieldKind::String,
394 crate::type_mapping::BinaryStrategy::Bytes
395 | crate::type_mapping::BinaryStrategy::VecU8 => MultipartFieldKind::Binary,
396 },
397 (Some("string"), _) => MultipartFieldKind::String,
398 (Some("integer"), Some("uint32" | "uint64" | "uint"))
399 if self.config.types.unsigned =>
400 {
401 MultipartFieldKind::UnsignedInteger
402 }
403 (Some("integer"), _) => MultipartFieldKind::Integer,
404 (Some("number"), _) => MultipartFieldKind::Number,
405 (Some("boolean"), _) => MultipartFieldKind::Boolean,
406 _ => {
407 return Err(ServerCodegenError::Validation(format!(
408 "multipart field `{wire_name}` must be binary or a scalar text field"
409 )));
410 }
411 };
412 Ok(MultipartFieldPlan {
413 wire_name: wire_name.clone(),
414 field_ident: CodeGenerator::to_field_ident(&wire_name.to_snake_case()),
415 required: required.contains(wire_name.as_str()),
416 kind,
417 })
418 })
419 .collect()
420 }
421
422 pub fn new(
423 config: &'a GeneratorConfig,
424 analysis: &'a SchemaAnalysis,
425 server: &'a ServerSection,
426 ) -> Self {
427 Self {
428 config,
429 analysis,
430 server,
431 source_provenance: None,
432 }
433 }
434
435 pub fn with_source_provenance(mut self, source: Option<&str>) -> Self {
437 self.source_provenance = source.map(str::to_string);
438 self
439 }
440
441 fn provenance_attribute(&self) -> TokenStream {
442 self.source_provenance
443 .as_ref()
444 .map(|source| {
445 let provenance = format!(
446 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
447 env!("CARGO_PKG_VERSION")
448 );
449 quote! { #![doc = #provenance] }
450 })
451 .unwrap_or_default()
452 }
453
454 fn model_type(&self, ty: &str) -> TokenStream {
460 if self.analysis.schemas.contains_key(ty) {
461 let canonical = rust_type_name(ty);
462 if canonical != ty {
463 let ident = format_ident!("{}", canonical);
464 return quote! { super::super::types::#ident };
465 }
466 }
467 parse_type(ty)
468 }
469
470 pub fn generate(&self) -> Result<Vec<GeneratedFile>, ServerCodegenError> {
472 if self.server.operations.is_empty() {
473 return Ok(Vec::new());
474 }
475 if !(1..=100).contains(&self.server.validation.max_errors) {
476 return Err(ServerCodegenError::Validation(
477 "max_errors must be between 1 and 100".to_string(),
478 ));
479 }
480 if !(1..=67_108_864).contains(&self.server.validation.max_body_bytes) {
481 return Err(ServerCodegenError::Validation(
482 "max_body_bytes must be between 1 and 67108864".to_string(),
483 ));
484 }
485
486 let index = OperationIndex::from_analysis(self.analysis);
487 let selectors: Vec<Selector> = self
488 .server
489 .operations
490 .iter()
491 .map(|s| Selector::parse(s))
492 .collect::<Result<_, _>>()?;
493 let resolution = super::resolve(&selectors, &index)?;
494
495 let ops: Vec<&OperationInfo> = resolution
499 .operations
500 .iter()
501 .map(|s| {
502 self.analysis
503 .operations
504 .get(&s.operation_id)
505 .ok_or_else(|| {
506 ServerCodegenError::Internal(format!(
507 "operation `{}` resolved but missing from analysis",
508 s.operation_id
509 ))
510 })
511 })
512 .collect::<Result<_, _>>()?;
513 validate_tag_identifier_collisions(&ops)?;
514 validate_custom_method_route_groups(&ops)?;
515 validate_normalized_route_collisions(&ops)?;
516 self.validate_query_parameters(&ops)?;
517 self.validate_supported_server_inputs(&ops)?;
518 self.validate_supported_server_outputs(&ops)?;
519 if !self.server.validation.enabled
520 && ops.iter().any(|operation| {
521 operation
522 .parameters
523 .iter()
524 .any(|parameter| matches!(parameter.location.as_str(), "header" | "cookie"))
525 || matches!(
526 &operation.request_body,
527 Some(RequestBodyContent::FormUrlEncoded { .. })
528 )
529 })
530 {
531 return Err(ServerCodegenError::Validation(
532 "typed header/cookie and form extraction requires server.validation.enabled=true"
533 .to_string(),
534 ));
535 }
536
537 let groups = group_by_tag(&ops);
539 let response_enum_names = self.allocate_response_enum_names(&ops);
540 let has_binary_body = ops.iter().any(|operation| {
541 matches!(
542 operation.request_body,
543 Some(RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. })
544 )
545 });
546 let has_text_body = ops.iter().any(|operation| {
547 matches!(
548 operation.request_body,
549 Some(RequestBodyContent::TextPlain { .. })
550 )
551 });
552 let has_embedded_path_affixes = ops.iter().any(|operation| {
553 operation.parameters.iter().any(|parameter| {
554 parameter.location == "path"
555 && path_parameter_affixes(&operation.path, ¶meter.name).is_some()
556 })
557 });
558
559 let validation_bundle = if self.server.validation.enabled {
560 Some(
561 super::validation::prepare_validation_bundle(
562 &self.analysis.validation_context,
563 &ops,
564 )
565 .map_err(|error| ServerCodegenError::Validation(error.to_string()))?,
566 )
567 } else {
568 eprintln!(
569 "⚠️ server.validation.enabled=false: generated handlers will not enforce the OpenAPI request contract"
570 );
571 None
572 };
573
574 let transport_validation = has_binary_body || has_text_body;
575 let validation_module_enabled =
576 validation_bundle.is_some() || transport_validation || has_embedded_path_affixes;
577 let api_rs = self.emit_api(&groups, &response_enum_names);
578 let errors_rs = self.emit_errors(&ops, validation_module_enabled, &response_enum_names);
579 let router_rs = self.emit_router(&groups, validation_bundle.as_ref())?;
580 let mod_rs = self.emit_mod(validation_module_enabled);
581
582 let mut files = vec![
583 GeneratedFile {
584 path: PathBuf::from("server").join("mod.rs"),
585 content: format_or_raw(mod_rs),
586 },
587 GeneratedFile {
588 path: PathBuf::from("server").join("api.rs"),
589 content: format_or_raw(api_rs),
590 },
591 GeneratedFile {
592 path: PathBuf::from("server").join("errors.rs"),
593 content: format_or_raw(errors_rs),
594 },
595 GeneratedFile {
596 path: PathBuf::from("server").join("router.rs"),
597 content: format_or_raw(router_rs),
598 },
599 ];
600 if let Some(bundle) = &validation_bundle {
601 files.push(GeneratedFile {
602 path: PathBuf::from("server").join("validation.rs"),
603 content: format_or_raw(super::validation::emit_validation_module(
604 bundle,
605 self.server.validation.max_errors,
606 has_binary_body,
607 has_text_body,
608 )),
609 });
610 } else if transport_validation || has_embedded_path_affixes {
611 files.push(GeneratedFile {
612 path: PathBuf::from("server").join("validation.rs"),
613 content: format_or_raw(super::validation::emit_transport_validation_module(
614 has_binary_body,
615 has_text_body,
616 )),
617 });
618 }
619 Ok(files)
620 }
621
622 fn query_parameter_type(&self, parameter: &ParameterInfo) -> TokenStream {
623 CodeGenerator::new(self.config.clone()).get_param_owned_rust_type(parameter)
624 }
625
626 fn parameter_schema_is_scalar(&self, schema: &serde_json::Value) -> bool {
627 self.parameter_schema_is_scalar_inner(schema, &mut std::collections::BTreeSet::new())
628 }
629
630 fn parameter_schema_is_string(&self, parameter: &ParameterInfo) -> bool {
631 if parameter.enum_values.is_some() || parameter.rust_type == "String" {
632 return true;
633 }
634 let Some(schema) = parameter.validation_schema.as_ref() else {
635 return false;
636 };
637 self.parameter_schema_is_string_inner(schema, &mut std::collections::BTreeSet::new())
638 }
639
640 fn parameter_schema_is_string_inner(
641 &self,
642 schema: &serde_json::Value,
643 visited: &mut std::collections::BTreeSet<String>,
644 ) -> bool {
645 if let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) {
646 if let Some(name) = reference.strip_prefix("#/components/schemas/") {
647 return self
648 .analysis
649 .validation_context
650 .component_schemas
651 .get(name)
652 .is_some_and(|component| {
653 visited.insert(name.to_string())
654 && self.parameter_schema_is_string_inner(component, visited)
655 });
656 }
657 }
658 schema.get("type").and_then(serde_json::Value::as_str) == Some("string")
659 }
660
661 fn parameter_schema_is_scalar_inner(
662 &self,
663 schema: &serde_json::Value,
664 visited: &mut std::collections::BTreeSet<String>,
665 ) -> bool {
666 if let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str)
667 && let Some(name) = reference.strip_prefix("#/components/schemas/")
668 && let Some(component) = self.analysis.validation_context.component_schemas.get(name)
669 {
670 return visited.insert(name.to_string())
671 && self.parameter_schema_is_scalar_inner(component, visited);
672 }
673 !matches!(
674 schema.get("type").and_then(serde_json::Value::as_str),
675 Some("array" | "object")
676 ) && schema.get("oneOf").is_none()
677 && schema.get("anyOf").is_none()
678 && schema.get("allOf").is_none()
679 }
680
681 fn form_field_names(
682 &self,
683 operation: &OperationInfo,
684 ) -> Result<Vec<String>, ServerCodegenError> {
685 let Some(RequestBodyContent::FormUrlEncoded { schema_name, .. }) = &operation.request_body
686 else {
687 return Ok(Vec::new());
688 };
689 let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
690 ServerCodegenError::UnsupportedRequestBody {
691 operation_id: operation.operation_id.clone(),
692 media_type: "application/x-www-form-urlencoded".to_string(),
693 reason: format!("schema `{schema_name}` cannot be resolved"),
694 }
695 })?;
696 let SchemaType::Object {
697 properties,
698 additional_properties,
699 ..
700 } = &schema.schema_type
701 else {
702 return Err(ServerCodegenError::UnsupportedRequestBody {
703 operation_id: operation.operation_id.clone(),
704 media_type: "application/x-www-form-urlencoded".to_string(),
705 reason: "only flat object schemas are supported".to_string(),
706 });
707 };
708 if additional_properties.is_open()
709 || properties.values().any(|property| {
710 !self.query_property_is_scalar(
711 &property.schema_type,
712 &mut std::collections::HashSet::new(),
713 )
714 })
715 {
716 return Err(ServerCodegenError::UnsupportedRequestBody {
717 operation_id: operation.operation_id.clone(),
718 media_type: "application/x-www-form-urlencoded".to_string(),
719 reason: "only flat scalar fields with additionalProperties forbidden are supported"
720 .to_string(),
721 });
722 }
723 Ok(properties.keys().cloned().collect())
724 }
725
726 fn validate_supported_server_inputs(
727 &self,
728 operations: &[&OperationInfo],
729 ) -> Result<(), ServerCodegenError> {
730 for operation in operations {
731 for parameter in &operation.parameters {
732 if !matches!(
733 parameter.location.as_str(),
734 "path" | "query" | "header" | "cookie"
735 ) {
736 return Err(ServerCodegenError::UnsupportedParameterSerialization {
737 operation_id: operation.operation_id.clone(),
738 location: parameter.location.clone(),
739 parameter: parameter.name.clone(),
740 });
741 }
742 if matches!(parameter.location.as_str(), "path" | "header" | "cookie")
743 && !matches!(
744 parameter.query_serialization,
745 Some(QuerySerialization::SimpleHeaderArray { .. })
746 )
747 && parameter
748 .validation_schema
749 .as_ref()
750 .is_none_or(|schema| !self.parameter_schema_is_scalar(schema))
751 {
752 return Err(ServerCodegenError::UnsupportedParameterSerialization {
753 operation_id: operation.operation_id.clone(),
754 location: parameter.location.clone(),
755 parameter: parameter.name.clone(),
756 });
757 }
758 }
759 match &operation.request_body {
760 Some(RequestBodyContent::FormUrlEncoded { .. }) => {
761 self.form_field_names(operation)?;
762 }
763 Some(RequestBodyContent::Multipart {
764 validation_schema, ..
765 }) => {
766 self.multipart_field_plans(validation_schema)?;
767 }
768 Some(RequestBodyContent::SchemaLess { media_type }) => {
769 return Err(ServerCodegenError::UnsupportedRequestBody {
770 operation_id: operation.operation_id.clone(),
771 media_type: media_type.clone(),
772 reason: "request content has no schema to validate".to_string(),
773 });
774 }
775 Some(RequestBodyContent::Unsupported { media_types }) => {
776 return Err(ServerCodegenError::UnsupportedRequestBody {
777 operation_id: operation.operation_id.clone(),
778 media_type: media_types.join(", "),
779 reason: "the selected request body uses unsupported media content"
780 .to_string(),
781 });
782 }
783 _ => {}
784 }
785 }
786 Ok(())
787 }
788
789 fn validate_supported_server_outputs(
790 &self,
791 operations: &[&OperationInfo],
792 ) -> Result<(), ServerCodegenError> {
793 for operation in operations {
794 if let Some(responses) = self
795 .analysis
796 .operation_responses
797 .get(&operation.operation_id)
798 {
799 for (status, response) in responses {
800 if response.has_content
805 && response.body.is_none()
806 && response.schema_name.is_none()
807 && !response.supports_streaming
808 {
809 return Err(ServerCodegenError::UnsupportedResponseContent {
810 operation_id: operation.operation_id.clone(),
811 status: status.clone(),
812 media_types: if response.unsupported_media_types.is_empty() {
813 "(schema-less content)".to_string()
814 } else {
815 response.unsupported_media_types.join(", ")
816 },
817 });
818 }
819 }
820 }
821 }
822 Ok(())
823 }
824
825 fn parameter_ident(&self, parameter: &ParameterInfo) -> syn::Ident {
826 let generator = CodeGenerator::new(self.config.clone());
827 CodeGenerator::to_field_ident(&generator.param_ident_str(parameter))
828 }
829
830 fn allocate_response_enum_names(&self, ops: &[&OperationInfo]) -> BTreeMap<String, syn::Ident> {
833 let generator = CodeGenerator::new(self.config.clone());
834 let mut used_names: std::collections::BTreeSet<String> = self
835 .analysis
836 .schemas
837 .keys()
838 .map(|name| generator.to_rust_type_name(name))
839 .collect();
840
841 used_names.extend(
845 ops.iter()
846 .flat_map(|operation| operation.parameters.iter())
847 .filter(|parameter| parameter.enum_values.is_some())
848 .map(|parameter| parameter.rust_type.clone()),
849 );
850
851 let mut sorted_ops = ops.to_vec();
854 sorted_ops.sort_by(|left, right| left.operation_id.cmp(&right.operation_id));
855
856 let mut names = BTreeMap::new();
857 for operation in sorted_ops {
858 let operation_name = operation.operation_id.to_pascal_case();
859 let preferred = format!("{operation_name}Response");
860 let chosen = if used_names.insert(preferred.clone()) {
861 preferred
862 } else {
863 let fallback = format!("{operation_name}ServerResponse");
864 let mut candidate = fallback.clone();
865 let mut suffix = 2;
866 while !used_names.insert(candidate.clone()) {
867 candidate = format!("{fallback}{suffix}");
868 suffix += 1;
869 }
870 candidate
871 };
872 names.insert(operation.operation_id.clone(), format_ident!("{chosen}"));
873 }
874 names
875 }
876
877 fn validation_target(
878 &self,
879 bundle: Option<&super::validation::ValidationBundle>,
880 operation: &OperationInfo,
881 location: &str,
882 parameter_name: Option<&str>,
883 ) -> Result<Option<TokenStream>, ServerCodegenError> {
884 let Some(bundle) = bundle else {
885 return Ok(None);
886 };
887 let target = bundle
888 .target_for(&operation.operation_id, location, parameter_name)
889 .ok_or_else(|| {
890 ServerCodegenError::Validation(format!(
891 "missing generated validator target for operation `{}` {location} `{}`",
892 operation.operation_id,
893 parameter_name.unwrap_or("body")
894 ))
895 })?;
896 let ident = format_ident!("{}", target.constant);
897 Ok(Some(quote! { super::validation::#ident }))
898 }
899
900 fn resolve_query_schema(&self, schema_name: &str) -> Option<&crate::analysis::AnalyzedSchema> {
901 let mut current = schema_name;
902 let mut visited = std::collections::HashSet::new();
903 loop {
904 if !visited.insert(current) {
905 return None;
906 }
907 let schema = self.analysis.schemas.get(current)?;
908 if let SchemaType::Reference { target } = &schema.schema_type {
909 current = target;
910 } else {
911 return Some(schema);
912 }
913 }
914 }
915
916 fn query_object_properties(
917 &self,
918 parameter: &ParameterInfo,
919 ) -> Option<&BTreeMap<String, crate::analysis::PropertyInfo>> {
920 let schema = self.resolve_query_schema(parameter.schema_ref.as_deref()?)?;
921 match &schema.schema_type {
922 SchemaType::Object { properties, .. } => Some(properties),
923 _ => None,
924 }
925 }
926
927 fn query_object_required_properties(&self, parameter: &ParameterInfo) -> Vec<String> {
928 let Some(schema) = parameter
929 .schema_ref
930 .as_deref()
931 .and_then(|name| self.resolve_query_schema(name))
932 else {
933 return Vec::new();
934 };
935 let mut names = match &schema.schema_type {
936 SchemaType::Object { required, .. } => required.iter().cloned().collect(),
937 _ => Vec::new(),
938 };
939 names.sort();
940 names
941 }
942
943 fn form_required_field_names(
944 &self,
945 operation: &OperationInfo,
946 ) -> Result<Vec<String>, ServerCodegenError> {
947 let Some(RequestBodyContent::FormUrlEncoded { schema_name, .. }) = &operation.request_body
948 else {
949 return Ok(Vec::new());
950 };
951 let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
952 ServerCodegenError::UnsupportedRequestBody {
953 operation_id: operation.operation_id.clone(),
954 media_type: "application/x-www-form-urlencoded".to_string(),
955 reason: format!("schema `{schema_name}` cannot be resolved"),
956 }
957 })?;
958 let mut names = match &schema.schema_type {
959 SchemaType::Object { required, .. } => required.iter().cloned().collect(),
960 _ => Vec::new(),
961 };
962 names.sort();
963 Ok(names)
964 }
965
966 fn query_property_is_scalar(
967 &self,
968 schema_type: &SchemaType,
969 visited: &mut std::collections::HashSet<String>,
970 ) -> bool {
971 match schema_type {
972 SchemaType::Primitive { .. }
973 | SchemaType::StringEnum { .. }
974 | SchemaType::ExtensibleEnum { .. } => true,
975 SchemaType::Reference { target } if visited.insert(target.clone()) => {
976 self.analysis.schemas.get(target).is_some_and(|schema| {
977 self.query_property_is_scalar(&schema.schema_type, visited)
978 })
979 }
980 _ => false,
981 }
982 }
983
984 fn validate_query_object(
985 &self,
986 operation: &OperationInfo,
987 parameter: &ParameterInfo,
988 ) -> Result<Vec<String>, ServerCodegenError> {
989 let error = |reason: String| ServerCodegenError::UnsupportedQueryParameter {
990 operation_id: operation.operation_id.clone(),
991 parameter: parameter.name.clone(),
992 reason,
993 };
994 let schema_name = parameter.schema_ref.as_deref().ok_or_else(|| {
995 error("styled object parameter has no analyzed schema type".to_string())
996 })?;
997 let schema = self.resolve_query_schema(schema_name).ok_or_else(|| {
998 error(format!(
999 "query object schema `{schema_name}` could not be resolved"
1000 ))
1001 })?;
1002 let (properties, additional_properties) = match &schema.schema_type {
1003 SchemaType::Object {
1004 properties,
1005 additional_properties,
1006 ..
1007 } => (properties, additional_properties),
1008 _ => {
1009 return Err(error(format!(
1010 "query schema `{schema_name}` does not resolve to a flat object"
1011 )));
1012 }
1013 };
1014 if additional_properties.is_open() {
1015 return Err(error(
1016 "styled object parameters with additionalProperties have an ambiguous wire namespace"
1017 .to_string(),
1018 ));
1019 }
1020 for (property_name, property) in properties {
1021 if !self.query_property_is_scalar(
1022 &property.schema_type,
1023 &mut std::collections::HashSet::new(),
1024 ) {
1025 return Err(error(format!(
1026 "property `{property_name}` is not scalar; nested arrays/objects are undefined for the generated query wire format"
1027 )));
1028 }
1029 }
1030 Ok(properties.keys().cloned().collect())
1031 }
1032
1033 fn validate_query_parameters(
1034 &self,
1035 operations: &[&OperationInfo],
1036 ) -> Result<(), ServerCodegenError> {
1037 for operation in operations {
1038 let mut claimed_keys: BTreeMap<String, String> = BTreeMap::new();
1039 for parameter in operation
1040 .parameters
1041 .iter()
1042 .filter(|parameter| parameter.location == "query")
1043 {
1044 let mut keys = match ¶meter.query_serialization {
1045 Some(QuerySerialization::Unsupported { reason }) => {
1046 return Err(ServerCodegenError::UnsupportedQueryParameter {
1047 operation_id: operation.operation_id.clone(),
1048 parameter: parameter.name.clone(),
1049 reason: reason.clone(),
1050 });
1051 }
1052 Some(
1053 QuerySerialization::FormExplodedObject
1054 | QuerySerialization::FormObject
1055 | QuerySerialization::DeepObject,
1056 ) => {
1057 let property_keys = self.validate_query_object(operation, parameter)?;
1058 if matches!(
1059 parameter.query_serialization,
1060 Some(QuerySerialization::FormExplodedObject)
1061 ) {
1062 property_keys
1063 } else if matches!(
1064 parameter.query_serialization,
1065 Some(QuerySerialization::DeepObject)
1066 ) {
1067 property_keys
1068 .into_iter()
1069 .map(|property| format!("{}[{property}]", parameter.name))
1070 .collect()
1071 } else {
1072 vec![parameter.name.clone()]
1073 }
1074 }
1075 Some(QuerySerialization::FormExplodedNestedObject { .. }) => {
1076 vec![parameter.name.clone()]
1077 }
1078 Some(
1079 QuerySerialization::FormExplodedArray { .. }
1080 | QuerySerialization::FormArray { .. }
1081 | QuerySerialization::SimpleHeaderArray { .. },
1082 )
1083 | None => vec![parameter.name.clone()],
1084 };
1085 if matches!(
1086 ¶meter.query_serialization,
1087 Some(
1088 QuerySerialization::FormExplodedObject
1089 | QuerySerialization::FormExplodedNestedObject { .. }
1090 | QuerySerialization::FormObject
1091 | QuerySerialization::DeepObject
1092 | QuerySerialization::FormExplodedArray { .. }
1093 | QuerySerialization::FormArray { .. }
1094 )
1095 ) {
1096 keys.push(format!("{}[]", parameter.name));
1097 }
1098 for key in keys {
1099 if let Some(first_parameter) =
1100 claimed_keys.insert(key.clone(), parameter.name.clone())
1101 {
1102 return Err(ServerCodegenError::AmbiguousQueryParameter {
1103 operation_id: operation.operation_id.clone(),
1104 wire_key: key,
1105 first_parameter,
1106 second_parameter: parameter.name.clone(),
1107 });
1108 }
1109 }
1110 }
1111 }
1112 Ok(())
1113 }
1114
1115 fn emit_mod(&self, validation_enabled: bool) -> TokenStream {
1116 let provenance_attribute = self.provenance_attribute();
1117 let validation_module = validation_enabled.then(|| quote! { pub(crate) mod validation; });
1118 quote! {
1119 #provenance_attribute
1125
1126 pub mod api;
1127 pub mod errors;
1128 pub mod router;
1129 #validation_module
1130
1131 pub use api::*;
1132 pub use errors::*;
1133 pub use router::*;
1134 }
1135 }
1136
1137 fn emit_router(
1138 &self,
1139 groups: &BTreeMap<String, Vec<&OperationInfo>>,
1140 validation_bundle: Option<&super::validation::ValidationBundle>,
1141 ) -> Result<TokenStream, ServerCodegenError> {
1142 let provenance_attribute = self.provenance_attribute();
1143 let factories: Vec<TokenStream> = groups
1144 .iter()
1145 .map(|(tag, ops)| self.emit_router_for_trait(tag, ops, validation_bundle))
1146 .collect::<Result<_, _>>()?;
1147
1148 let query_structs: Vec<TokenStream> = groups
1150 .values()
1151 .flatten()
1152 .filter_map(|op| self.emit_query_struct(op))
1153 .collect();
1154 let has_query_parameters = groups.values().flatten().any(|operation| {
1155 operation
1156 .parameters
1157 .iter()
1158 .any(|parameter| parameter.location == "query")
1159 });
1160 let query_helpers = has_query_parameters.then(|| {
1161 quote! {
1162 fn __query_pairs(raw: ::std::option::Option<&str>) -> ::std::vec::Vec<(String, String)> {
1163 raw.map(|query| {
1164 ::url::form_urlencoded::parse(query.as_bytes())
1165 .into_owned()
1166 .collect()
1167 })
1168 .unwrap_or_default()
1169 }
1170
1171 fn __validate_urlencoded(raw: &str) -> ::std::result::Result<(), String> {
1172 let bytes = raw.as_bytes();
1173 let mut index = 0;
1174 while index < bytes.len() {
1175 if bytes[index] == b'%' {
1176 if index + 2 >= bytes.len()
1177 || !bytes[index + 1].is_ascii_hexdigit()
1178 || !bytes[index + 2].is_ascii_hexdigit()
1179 {
1180 return Err("malformed percent encoding".to_string());
1181 }
1182 index += 3;
1183 } else {
1184 index += 1;
1185 }
1186 }
1187 Ok(())
1188 }
1189
1190 fn __query_one(
1191 pairs: &[(String, String)],
1192 key: &str,
1193 ) -> ::std::result::Result<::std::option::Option<String>, String> {
1194 let mut values = pairs
1195 .iter()
1196 .filter(|(candidate, _)| candidate == key)
1197 .map(|(_, value)| value.clone());
1198 let value = values.next();
1199 if values.next().is_some() {
1200 return Err(format!("query parameter `{key}` appeared more than once"));
1201 }
1202 Ok(value)
1203 }
1204
1205 fn __decode_query_scalar<T>(
1206 value: &str,
1207 label: &str,
1208 ) -> ::std::result::Result<T, String>
1209 where
1210 T: ::serde::de::DeserializeOwned,
1211 {
1212 ::serde_json::from_value(::serde_json::Value::String(value.to_string()))
1213 .or_else(|_| ::serde_json::from_str(value))
1214 .map_err(|error| format!("invalid query value for `{label}`: {error}"))
1215 }
1216
1217 fn __decode_query_object<T>(
1218 fields: &[(String, String)],
1219 label: &str,
1220 ) -> ::std::result::Result<T, String>
1221 where
1222 T: ::serde::de::DeserializeOwned,
1223 {
1224 let mut serializer =
1225 ::url::form_urlencoded::Serializer::new(String::new());
1226 for (key, value) in fields {
1227 serializer.append_pair(key, value);
1228 }
1229 ::serde_urlencoded::from_str(&serializer.finish())
1230 .map_err(|error| format!("invalid query object `{label}`: {error}"))
1231 }
1232
1233 fn __query_empty_marker(
1234 pairs: &[(String, String)],
1235 key: &str,
1236 ) -> ::std::result::Result<bool, String> {
1237 let marker = format!("{key}[]");
1238 match __query_one(pairs, &marker)? {
1239 Some(value) if value.is_empty() => Ok(true),
1240 Some(_) => Err(format!(
1241 "zero-cardinality marker `{marker}` must have an empty value"
1242 )),
1243 None => Ok(false),
1244 }
1245 }
1246 }
1247 });
1248
1249 let combined = if groups.len() > 1 {
1255 Some(self.emit_combined_router(groups))
1256 } else {
1257 None
1258 };
1259
1260 Ok(quote! {
1261 #provenance_attribute
1266
1267 use super::api::*;
1268 use super::errors::*;
1269 #[allow(unused_imports)]
1274 use super::super::types::*;
1275
1276 #query_helpers
1277
1278 #(#query_structs)*
1279
1280 #(#factories)*
1281
1282 #combined
1283 })
1284 }
1285
1286 fn emit_combined_router(&self, groups: &BTreeMap<String, Vec<&OperationInfo>>) -> TokenStream {
1287 let entries: Vec<(syn::Ident, syn::Ident, syn::Ident)> = groups
1291 .keys()
1292 .enumerate()
1293 .map(|(i, tag)| {
1294 let trait_ident = trait_ident_for_tag(tag);
1295 let factory = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
1296 let generic = format_ident!("T{}", i + 1);
1297 (trait_ident, factory, generic)
1298 })
1299 .collect();
1300
1301 let generics: Vec<&syn::Ident> = entries.iter().map(|(_, _, g)| g).collect();
1302 let args: Vec<TokenStream> = entries
1303 .iter()
1304 .map(|(trait_ident, _, g)| {
1305 let arg_ident = format_ident!("{}", trait_ident.to_string().to_snake_case());
1306 quote! { #arg_ident: #g }
1307 })
1308 .collect();
1309 let bounds: Vec<TokenStream> = entries
1310 .iter()
1311 .map(|(trait_ident, _, g)| {
1312 quote! { #g: #trait_ident + Clone + Send + Sync + 'static }
1313 })
1314 .collect();
1315
1316 let first = &entries[0];
1318 let first_arg = format_ident!("{}", first.0.to_string().to_snake_case());
1319 let first_factory = &first.1;
1320 let rest = entries
1321 .iter()
1322 .skip(1)
1323 .map(|(trait_ident, factory, _)| {
1324 let arg = format_ident!("{}", trait_ident.to_string().to_snake_case());
1325 quote! { .merge(#factory(#arg)) }
1326 })
1327 .collect::<Vec<_>>();
1328
1329 let trait_names: Vec<String> = entries.iter().map(|(t, _, _)| t.to_string()).collect();
1330 let doc = format!(
1331 " Combined router spanning {} traits: {}.",
1332 entries.len(),
1333 trait_names.join(", "),
1334 );
1335
1336 quote! {
1337 #[doc = #doc]
1338 pub fn build_router<#(#generics),*>(
1339 #(#args),*
1340 ) -> ::axum::Router
1341 where
1342 #(#bounds),*
1343 {
1344 #first_factory(#first_arg) #(#rest)*
1345 }
1346 }
1347 }
1348
1349 fn emit_router_for_trait(
1350 &self,
1351 tag: &str,
1352 ops: &[&OperationInfo],
1353 validation_bundle: Option<&super::validation::ValidationBundle>,
1354 ) -> Result<TokenStream, ServerCodegenError> {
1355 let trait_ident = trait_ident_for_tag(tag);
1356 let fn_ident = format_ident!("{}_router", trait_ident.to_string().to_snake_case());
1357
1358 let mut routes: Vec<TokenStream> = Vec::new();
1359 let mut custom_by_path: BTreeMap<String, Vec<(String, syn::Ident)>> = BTreeMap::new();
1360 for op in ops {
1361 let handler = format_ident!("{}_handler", op.operation_id.to_snake_case());
1362 let path = openapi_to_axum_path(&op.path)?;
1363 if let Some(method_call) = axum_method_call(&op.method) {
1364 routes.push(quote! { .route(#path, ::axum::routing::#method_call(#handler::<T>)) });
1365 } else {
1366 custom_by_path
1367 .entry(path)
1368 .or_default()
1369 .push((op.method.to_ascii_uppercase(), handler));
1370 }
1371 }
1372 let mut custom_dispatchers = Vec::new();
1373 for (path, methods) in custom_by_path {
1374 let first_handler = &methods[0].1;
1375 let dispatcher = format_ident!("{}_custom_method_dispatch", first_handler);
1376 let (route, dispatcher_fn) =
1377 axum_custom_route(&path, &dispatcher, &methods, &trait_ident);
1378 routes.push(route);
1379 custom_dispatchers.push(dispatcher_fn);
1380 }
1381
1382 let handlers: Vec<TokenStream> = ops
1383 .iter()
1384 .map(|op| self.emit_axum_handler(&trait_ident, op, validation_bundle))
1385 .collect::<Result<_, _>>()?;
1386
1387 let doc = format!(" Build an axum::Router for the `{trait_ident}` trait.");
1388 let max_body_bytes = self.server.validation.max_body_bytes;
1389
1390 Ok(quote! {
1391 #[doc = #doc]
1392 pub fn #fn_ident<T>(api: T) -> ::axum::Router
1393 where
1394 T: #trait_ident + Clone + Send + Sync + 'static,
1395 {
1396 ::axum::Router::new()
1397 #(#routes)*
1398 .layer(::axum::extract::DefaultBodyLimit::max(#max_body_bytes))
1399 .with_state(api)
1400 }
1401
1402 #(#custom_dispatchers)*
1403
1404 #(#handlers)*
1405 })
1406 }
1407
1408 fn emit_axum_handler(
1409 &self,
1410 trait_ident: &syn::Ident,
1411 op: &OperationInfo,
1412 validation_bundle: Option<&super::validation::ValidationBundle>,
1413 ) -> Result<TokenStream, ServerCodegenError> {
1414 let handler_ident = format_ident!("{}_handler", op.operation_id.to_snake_case());
1415 let trait_method = format_ident!("{}", op.operation_id.to_snake_case());
1416
1417 let mut extractors: Vec<TokenStream> =
1419 vec![quote! { ::axum::extract::State(api): ::axum::extract::State<T> }];
1420 let mut call_args: Vec<TokenStream> = Vec::new();
1421
1422 let path_params: Vec<&_> = op
1426 .parameters
1427 .iter()
1428 .filter(|p| p.location == "path")
1429 .collect();
1430 let path_has_affixes = path_params
1431 .iter()
1432 .any(|parameter| path_parameter_affixes(&op.path, ¶meter.name).is_some());
1433 let mut path_decode = TokenStream::new();
1434 if !path_params.is_empty() && (validation_bundle.is_some() || path_has_affixes) {
1435 extractors.push(quote! {
1436 __path_result: ::std::result::Result<
1437 ::axum::extract::Path<::std::collections::HashMap<String, String>>,
1438 ::axum::extract::rejection::PathRejection,
1439 >
1440 });
1441 let mut decoders = Vec::new();
1442 for parameter in &path_params {
1443 let ident = self.parameter_ident(parameter);
1444 let ty = self.query_parameter_type(parameter);
1445 let wire = parameter.name.as_str();
1446 let location = parameter_location("path", wire);
1447 let target = self.validation_target(validation_bundle, op, "path", Some(wire))?;
1448 let string_wire = self.parameter_schema_is_string(parameter);
1449 let strip_affixes = match path_parameter_affixes(&op.path, wire) {
1450 Some((prefix, suffix)) => quote! {
1451 let raw = match raw
1452 .strip_prefix(#prefix)
1453 .and_then(|value| value.strip_suffix(#suffix))
1454 {
1455 Some(value) => value.to_string(),
1456 None => return ::axum::response::IntoResponse::into_response(
1457 ::axum::http::StatusCode::NOT_FOUND
1458 ),
1459 };
1460 },
1461 None => TokenStream::new(),
1462 };
1463 let decode = if let Some(target) = target {
1464 quote! {
1465 match super::validation::decode_parameter(
1466 &raw, #target, #location, #string_wire,
1467 ) {
1468 Ok(value) => value,
1469 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1470 }
1471 }
1472 } else {
1473 quote! {
1474 match ::serde_json::from_value(::serde_json::Value::String(raw.clone()))
1475 .or_else(|_| ::serde_json::from_str(&raw))
1476 {
1477 Ok(value) => value,
1478 Err(_) => return ::axum::response::IntoResponse::into_response(
1479 ::axum::http::StatusCode::BAD_REQUEST
1480 ),
1481 }
1482 }
1483 };
1484 decoders.push(quote! {
1485 let #ident: #ty = match __path_values.remove(#wire) {
1486 Some(raw) => {
1487 #strip_affixes
1488 #decode
1489 },
1490 None => return ::axum::response::IntoResponse::into_response(
1491 ::axum::http::StatusCode::INTERNAL_SERVER_ERROR
1492 ),
1493 };
1494 });
1495 call_args.push(quote! { #ident });
1496 }
1497 path_decode = quote! {
1498 let ::axum::extract::Path(mut __path_values) = match __path_result {
1499 Ok(path) => path,
1500 Err(_) => return ::axum::response::IntoResponse::into_response(
1501 ::axum::http::StatusCode::BAD_REQUEST
1502 ),
1503 };
1504 #(#decoders)*
1505 };
1506 } else if !path_params.is_empty() {
1507 let idents: Vec<syn::Ident> = path_params
1508 .iter()
1509 .map(|p| self.parameter_ident(p))
1510 .collect();
1511 let types: Vec<TokenStream> = path_params
1512 .iter()
1513 .map(|p| self.query_parameter_type(p))
1514 .collect();
1515 if path_params.len() == 1 {
1516 let i = &idents[0];
1517 let t = &types[0];
1518 extractors.push(quote! { ::axum::extract::Path(#i): ::axum::extract::Path<#t> });
1519 } else {
1520 extractors.push(quote! { ::axum::extract::Path((#(#idents),*)): ::axum::extract::Path<(#(#types),*)> });
1521 }
1522 for i in &idents {
1523 call_args.push(quote! { #i });
1524 }
1525 }
1526
1527 let query_params: Vec<&_> = op
1532 .parameters
1533 .iter()
1534 .filter(|p| p.location == "query")
1535 .collect();
1536 let mut required_query_checks: Vec<TokenStream> = Vec::new();
1537 let mut query_validation_checks: Vec<TokenStream> = Vec::new();
1538 let mut raw_query_validation_checks: Vec<TokenStream> = Vec::new();
1539 let mut query_decode = TokenStream::new();
1540 if !query_params.is_empty() {
1541 let query_ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
1542 let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
1543 extractors.push(quote! {
1544 ::axum::extract::RawQuery(__raw_query): ::axum::extract::RawQuery
1545 });
1546 query_decode = if validation_bundle.is_some() {
1547 quote! {
1548 let __q: #query_ident = match #decode_ident(__raw_query.as_deref()) {
1549 Ok(query) => query,
1550 Err(_) => return ::axum::response::IntoResponse::into_response(
1551 super::validation::malformed_parameter("/query")
1552 ),
1553 };
1554 }
1555 } else {
1556 quote! {
1557 let __q: #query_ident = match #decode_ident(__raw_query.as_deref()) {
1558 Ok(query) => query,
1559 Err(message) => return ::axum::response::IntoResponse::into_response(
1560 (
1561 ::axum::http::StatusCode::BAD_REQUEST,
1562 ::axum::Json(::serde_json::json!({ "error": message })),
1563 )
1564 ),
1565 };
1566 }
1567 };
1568 for p in &query_params {
1569 let f = self.parameter_ident(p);
1570 let wire = p.name.as_str();
1571 let location = parameter_location("query", wire);
1572 let target = self.validation_target(validation_bundle, op, "query", Some(wire))?;
1573 if p.query_serialization.is_none() && self.parameter_schema_is_string(p) {
1574 if let Some(target) = target.as_ref() {
1575 raw_query_validation_checks.push(quote! {
1576 if let Ok(Some(raw)) = __query_one(&__raw_query_pairs, #wire) {
1577 if let Err(rejection) = super::validation::validate_string_parameter(
1578 #target, #location, &raw,
1579 ) {
1580 return ::axum::response::IntoResponse::into_response(rejection);
1581 }
1582 }
1583 });
1584 }
1585 }
1586 if p.required {
1587 required_query_checks.push(if validation_bundle.is_some() {
1588 quote! {
1589 let #f = match __q.#f {
1590 Some(v) => v,
1591 None => return ::axum::response::IntoResponse::into_response(
1592 super::validation::missing_parameter(#location)
1593 ),
1594 };
1595 }
1596 } else {
1597 let missing_msg = format!("missing required query parameter `{wire}`");
1598 quote! {
1599 let #f = match __q.#f {
1600 Some(v) => v,
1601 None => return ::axum::response::IntoResponse::into_response(
1602 (
1603 ::axum::http::StatusCode::BAD_REQUEST,
1604 ::axum::Json(::serde_json::json!({
1605 "error": #missing_msg
1606 })),
1607 )
1608 ),
1609 };
1610 }
1611 });
1612 if let Some(target) = target {
1613 query_validation_checks.push(quote! {
1614 if let Err(rejection) = super::validation::validate_parameter(
1615 #target, #location, &#f,
1616 ) {
1617 return ::axum::response::IntoResponse::into_response(rejection);
1618 }
1619 });
1620 }
1621 call_args.push(quote! { #f });
1622 } else {
1623 if let Some(target) = target {
1624 query_validation_checks.push(quote! {
1625 if let Some(value) = &__q.#f {
1626 if let Err(rejection) = super::validation::validate_parameter(
1627 #target, #location, value,
1628 ) {
1629 return ::axum::response::IntoResponse::into_response(rejection);
1630 }
1631 }
1632 });
1633 }
1634 call_args.push(quote! { __q.#f });
1635 }
1636 }
1637 }
1638
1639 let header_params: Vec<&_> = op
1643 .parameters
1644 .iter()
1645 .filter(|p| p.location == "header")
1646 .collect();
1647 let cookie_params: Vec<&_> = op
1648 .parameters
1649 .iter()
1650 .filter(|p| p.location == "cookie")
1651 .collect();
1652 let mut parameter_decode_checks: Vec<TokenStream> = Vec::new();
1653 if !header_params.is_empty() || !cookie_params.is_empty() {
1654 extractors.push(quote! { __headers: ::axum::http::HeaderMap });
1655 }
1656 if !header_params.is_empty() {
1657 for p in &header_params {
1658 let wire = p.name.as_str();
1659 let ident = self.parameter_ident(p);
1660 let ty = self.query_parameter_type(p);
1661 let location = parameter_location("header", wire);
1662 let target = self.validation_target(validation_bundle, op, "header", Some(wire))?;
1663 let string_wire = self.parameter_schema_is_string(p);
1664 if matches!(
1665 p.query_serialization,
1666 Some(QuerySerialization::SimpleHeaderArray { .. })
1667 ) {
1668 let decode_array = quote! {
1669 raw.split(',')
1670 .map(|item| __decode_query_scalar(item, #wire))
1671 .collect::<::std::result::Result<#ty, _>>()
1672 };
1673 if p.required {
1674 parameter_decode_checks.push(quote! {
1675 let mut __values = __headers.get_all(#wire).iter();
1676 let #ident: #ty = match (__values.next(), __values.next()) {
1677 (Some(value), None) => match value.to_str() {
1678 Ok(raw) => match #decode_array {
1679 Ok(value) => value,
1680 Err(_) => return ::axum::response::IntoResponse::into_response(
1681 super::validation::malformed_parameter(#location)
1682 ),
1683 },
1684 Err(_) => return ::axum::response::IntoResponse::into_response(
1685 super::validation::malformed_parameter(#location)
1686 ),
1687 },
1688 (None, _) => return ::axum::response::IntoResponse::into_response(
1689 super::validation::missing_parameter(#location)
1690 ),
1691 _ => return ::axum::response::IntoResponse::into_response(
1692 super::validation::malformed_parameter(#location)
1693 ),
1694 };
1695 });
1696 if let Some(target) = target {
1697 parameter_decode_checks.push(quote! {
1698 if let Err(rejection) = super::validation::validate_parameter(
1699 #target, #location, &#ident,
1700 ) {
1701 return ::axum::response::IntoResponse::into_response(rejection);
1702 }
1703 });
1704 }
1705 call_args.push(quote! { #ident });
1706 } else {
1707 parameter_decode_checks.push(quote! {
1708 let mut __values = __headers.get_all(#wire).iter();
1709 let #ident: ::std::option::Option<#ty> = match (__values.next(), __values.next()) {
1710 (Some(value), None) => match value.to_str() {
1711 Ok(raw) => match #decode_array {
1712 Ok(value) => Some(value),
1713 Err(_) => return ::axum::response::IntoResponse::into_response(
1714 super::validation::malformed_parameter(#location)
1715 ),
1716 },
1717 Err(_) => return ::axum::response::IntoResponse::into_response(
1718 super::validation::malformed_parameter(#location)
1719 ),
1720 },
1721 (None, _) => None,
1722 _ => return ::axum::response::IntoResponse::into_response(
1723 super::validation::malformed_parameter(#location)
1724 ),
1725 };
1726 });
1727 if let Some(target) = target {
1728 parameter_decode_checks.push(quote! {
1729 if let Some(value) = &#ident {
1730 if let Err(rejection) = super::validation::validate_parameter(
1731 #target, #location, value,
1732 ) {
1733 return ::axum::response::IntoResponse::into_response(rejection);
1734 }
1735 }
1736 });
1737 }
1738 call_args.push(quote! { #ident });
1739 }
1740 continue;
1741 }
1742 if p.required {
1743 if let Some(target) = target {
1744 parameter_decode_checks.push(quote! {
1745 let mut __values = __headers.get_all(#wire).iter();
1746 let #ident: #ty = match (__values.next(), __values.next()) {
1747 (Some(value), None) => match value.to_str() {
1748 Ok(raw) => match super::validation::decode_parameter(
1749 raw, #target, #location, #string_wire,
1750 ) {
1751 Ok(value) => value,
1752 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1753 },
1754 Err(_) => return ::axum::response::IntoResponse::into_response(
1755 super::validation::malformed_parameter(#location)
1756 ),
1757 },
1758 (None, _) => return ::axum::response::IntoResponse::into_response(
1759 super::validation::missing_parameter(#location)
1760 ),
1761 _ => return ::axum::response::IntoResponse::into_response(
1762 super::validation::malformed_parameter(#location)
1763 ),
1764 };
1765 });
1766 } else {
1767 parameter_decode_checks.push(quote! {
1768 let #ident: #ty = match __headers.get(#wire).and_then(|v| v.to_str().ok()) {
1769 Some(raw) => match __decode_query_scalar(raw, #wire) {
1770 Ok(value) => value,
1771 Err(_) => return ::axum::http::StatusCode::BAD_REQUEST.into_response(),
1772 },
1773 None => return ::axum::http::StatusCode::BAD_REQUEST.into_response(),
1774 };
1775 });
1776 }
1777 call_args.push(quote! { #ident });
1778 } else {
1779 if let Some(target) = target {
1780 parameter_decode_checks.push(quote! {
1781 let mut __values = __headers.get_all(#wire).iter();
1782 let #ident: ::std::option::Option<#ty> = match (__values.next(), __values.next()) {
1783 (Some(value), None) => match value.to_str() {
1784 Ok(raw) => match super::validation::decode_parameter(raw, #target, #location, #string_wire) {
1785 Ok(value) => Some(value),
1786 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1787 },
1788 Err(_) => return ::axum::response::IntoResponse::into_response(
1789 super::validation::malformed_parameter(#location)
1790 ),
1791 },
1792 (None, _) => None,
1793 _ => return ::axum::response::IntoResponse::into_response(
1794 super::validation::malformed_parameter(#location)
1795 ),
1796 };
1797 });
1798 } else {
1799 parameter_decode_checks.push(quote! {
1800 let #ident: ::std::option::Option<#ty> = __headers.get(#wire)
1801 .and_then(|value| value.to_str().ok())
1802 .and_then(|raw| __decode_query_scalar(raw, #wire).ok());
1803 });
1804 }
1805 call_args.push(quote! { #ident });
1806 }
1807 }
1808 }
1809 if !cookie_params.is_empty() {
1810 parameter_decode_checks.push(quote! {
1811 let mut __cookies = match super::validation::parse_cookies(&__headers) {
1812 Ok(cookies) => cookies,
1813 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1814 };
1815 });
1816 for p in &cookie_params {
1817 let wire = p.name.as_str();
1818 let ident = self.parameter_ident(p);
1819 let ty = self.query_parameter_type(p);
1820 let location = parameter_location("cookie", wire);
1821 let target = self
1822 .validation_target(validation_bundle, op, "cookie", Some(wire))?
1823 .ok_or_else(|| {
1824 ServerCodegenError::Validation(format!(
1825 "cookie extraction requires validation for operation `{}`",
1826 op.operation_id
1827 ))
1828 })?;
1829 let string_wire = self.parameter_schema_is_string(p);
1830 if p.required {
1831 parameter_decode_checks.push(quote! {
1832 let #ident: #ty = match __cookies.remove(#wire) {
1833 Some(raw) => match super::validation::decode_parameter(&raw, #target, #location, #string_wire) {
1834 Ok(value) => value,
1835 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1836 },
1837 None => return ::axum::response::IntoResponse::into_response(
1838 super::validation::missing_parameter(#location)
1839 ),
1840 };
1841 });
1842 call_args.push(quote! { #ident });
1843 } else {
1844 parameter_decode_checks.push(quote! {
1845 let #ident: ::std::option::Option<#ty> = match __cookies.remove(#wire) {
1846 Some(raw) => match super::validation::decode_parameter(&raw, #target, #location, #string_wire) {
1847 Ok(value) => Some(value),
1848 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1849 },
1850 None => None,
1851 };
1852 });
1853 call_args.push(quote! { #ident });
1854 }
1855 }
1856 }
1857
1858 let mut body_decode = TokenStream::new();
1860 let body_ty_opt = body_type(op);
1861 if let Some(body_ty) = &body_ty_opt {
1862 let body_ty_tokens = self.model_type(body_ty);
1863 let transport_body = match &op.request_body {
1864 Some(RequestBodyContent::OctetStream { media_type }) => {
1865 Some((format_ident!("decode_binary_body"), media_type.clone()))
1866 }
1867 Some(RequestBodyContent::Binary { media_type }) => {
1868 Some((format_ident!("decode_binary_body"), media_type.clone()))
1869 }
1870 Some(RequestBodyContent::TextPlain { media_type }) => {
1871 Some((format_ident!("decode_text_body"), media_type.clone()))
1872 }
1873 _ => None,
1874 };
1875 let validated_json = matches!(&op.request_body, Some(RequestBodyContent::Json { .. }))
1876 && validation_bundle.is_some();
1877 let validated_form = matches!(
1878 &op.request_body,
1879 Some(RequestBodyContent::FormUrlEncoded { .. })
1880 ) && validation_bundle.is_some();
1881 let typed_multipart =
1882 matches!(&op.request_body, Some(RequestBodyContent::Multipart { .. }));
1883 if let Some((decoder, media_type)) = transport_body {
1884 extractors.push(quote! { __request: ::axum::extract::Request });
1885 let required = op.request_body_required;
1886 let max_body_bytes = self.server.validation.max_body_bytes;
1887 body_decode = quote! {
1888 let body: ::std::option::Option<#body_ty_tokens> =
1889 match super::validation::#decoder(
1890 __request,
1891 #media_type,
1892 #required,
1893 #max_body_bytes,
1894 ).await {
1895 Ok(body) => body,
1896 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
1897 };
1898 };
1899 if required {
1900 body_decode.extend(quote! {
1901 let body = match body {
1902 Some(body) => body,
1903 None => return ::axum::response::IntoResponse::into_response(
1904 super::validation::generated_contract_error()
1905 ),
1906 };
1907 });
1908 }
1909 call_args.push(quote! { body });
1910 } else if typed_multipart {
1911 extractors.push(quote! { __request: ::axum::extract::Request });
1912 let Some(RequestBodyContent::Multipart {
1913 validation_schema, ..
1914 }) = &op.request_body
1915 else {
1916 return Err(ServerCodegenError::Internal(
1917 "typed multipart body lost its schema".to_string(),
1918 ));
1919 };
1920 let plans = self.multipart_field_plans(validation_schema)?;
1921 let multipart_validation = self
1922 .validation_target(validation_bundle, op, "body", None)?
1923 .map(|target| {
1924 quote! {
1925 if let Err(rejection) = super::validation::validate_parameter(
1926 #target,
1927 "/body",
1928 &::serde_json::Value::Object(validation_object),
1929 ) {
1930 return ::axum::response::IntoResponse::into_response(rejection);
1931 }
1932 }
1933 })
1934 .unwrap_or_default();
1935 let mut binary_locals = Vec::new();
1936 let mut binary_validation_arms = Vec::new();
1937 let mut binary_patches = Vec::new();
1938 for plan in plans
1939 .iter()
1940 .filter(|plan| matches!(plan.kind, MultipartFieldKind::Binary))
1941 {
1942 let wire_name = &plan.wire_name;
1943 let field_ident = &plan.field_ident;
1944 let slot = format_ident!("__multipart_binary_{}", field_ident);
1945 binary_locals.push(quote! {
1946 let mut #slot: ::std::option::Option<::bytes::Bytes> = None;
1947 });
1948 binary_validation_arms.push(quote! {
1949 #wire_name => ::serde_json::Value::String(
1950 "x".repeat(#slot.as_ref().map_or(0, ::bytes::Bytes::len))
1951 ),
1952 });
1953 binary_patches.push(match (self.config.types.binary, plan.required) {
1954 (crate::type_mapping::BinaryStrategy::Bytes, true) => quote! {
1955 body.#field_ident = match #slot {
1956 Some(bytes) => bytes,
1957 None => return ::axum::response::IntoResponse::into_response(
1958 (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "missing required multipart field")
1959 ),
1960 };
1961 },
1962 (crate::type_mapping::BinaryStrategy::Bytes, false) => quote! {
1963 body.#field_ident = #slot;
1964 },
1965 (crate::type_mapping::BinaryStrategy::VecU8, true) => quote! {
1966 body.#field_ident = match #slot {
1967 Some(bytes) => bytes.to_vec(),
1968 None => return ::axum::response::IntoResponse::into_response(
1969 (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "missing required multipart field")
1970 ),
1971 };
1972 },
1973 (crate::type_mapping::BinaryStrategy::VecU8, false) => quote! {
1974 body.#field_ident = #slot.map(|bytes| bytes.to_vec());
1975 },
1976 (crate::type_mapping::BinaryStrategy::String, _) => unreachable!(
1977 "string-backed binary fields must use multipart text extraction"
1978 ),
1979 });
1980 }
1981 let mut field_arms = Vec::new();
1982 for plan in plans {
1983 let wire_name = plan.wire_name;
1984 let binary_slot = format_ident!("__multipart_binary_{}", plan.field_ident);
1985 let decode = match plan.kind {
1986 MultipartFieldKind::Binary => quote! {
1987 let bytes = match field.bytes().await {
1988 Ok(bytes) => bytes,
1989 Err(_) => return ::axum::response::IntoResponse::into_response(
1990 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart field")
1991 ),
1992 };
1993 #binary_slot = Some(bytes);
1994 ::serde_json::Value::Array(Vec::new())
1995 },
1996 MultipartFieldKind::String => quote! {
1997 match field.text().await {
1998 Ok(text) => ::serde_json::Value::String(text),
1999 Err(_) => return ::axum::response::IntoResponse::into_response(
2000 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart text field")
2001 ),
2002 }
2003 },
2004 MultipartFieldKind::Integer => quote! {
2005 let text = match field.text().await {
2006 Ok(text) => text,
2007 Err(_) => return ::axum::response::IntoResponse::into_response(
2008 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart integer field")
2009 ),
2010 };
2011 match text.parse::<i64>() {
2012 Ok(value) => ::serde_json::Value::Number(value.into()),
2013 Err(_) => return ::axum::response::IntoResponse::into_response(
2014 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart integer field")
2015 ),
2016 }
2017 },
2018 MultipartFieldKind::UnsignedInteger => quote! {
2019 let text = match field.text().await {
2020 Ok(text) => text,
2021 Err(_) => return ::axum::response::IntoResponse::into_response(
2022 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart unsigned integer field")
2023 ),
2024 };
2025 match text.parse::<u64>() {
2026 Ok(value) => ::serde_json::Value::Number(value.into()),
2027 Err(_) => return ::axum::response::IntoResponse::into_response(
2028 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart unsigned integer field")
2029 ),
2030 }
2031 },
2032 MultipartFieldKind::Number => quote! {
2033 let text = match field.text().await {
2034 Ok(text) => text,
2035 Err(_) => return ::axum::response::IntoResponse::into_response(
2036 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart number field")
2037 ),
2038 };
2039 match text.parse::<f64>().ok().and_then(::serde_json::Number::from_f64) {
2040 Some(value) => ::serde_json::Value::Number(value),
2041 None => return ::axum::response::IntoResponse::into_response(
2042 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart number field")
2043 ),
2044 }
2045 },
2046 MultipartFieldKind::Boolean => quote! {
2047 let text = match field.text().await {
2048 Ok(text) => text,
2049 Err(_) => return ::axum::response::IntoResponse::into_response(
2050 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart boolean field")
2051 ),
2052 };
2053 match text.parse::<bool>() {
2054 Ok(value) => ::serde_json::Value::Bool(value),
2055 Err(_) => return ::axum::response::IntoResponse::into_response(
2056 (::axum::http::StatusCode::BAD_REQUEST, "invalid multipart boolean field")
2057 ),
2058 }
2059 },
2060 };
2061 field_arms.push(quote! {
2062 #wire_name => { #decode }
2063 });
2064 }
2065 let required = op.request_body_required;
2066 body_decode = quote! {
2067 let __has_multipart_content_type = __request.headers()
2068 .get(::axum::http::header::CONTENT_TYPE)
2069 .is_some();
2070 let body: ::std::option::Option<#body_ty_tokens> =
2071 if !__has_multipart_content_type && !#required {
2072 None
2073 } else {
2074 let mut multipart = match <::axum::extract::Multipart as ::axum::extract::FromRequest<T>>::from_request(__request, &api).await {
2075 Ok(multipart) => multipart,
2076 Err(_) => return ::axum::response::IntoResponse::into_response(
2077 (::axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE, "invalid multipart request")
2078 ),
2079 };
2080 let mut object = ::serde_json::Map::new();
2081 let mut validation_object = ::serde_json::Map::new();
2082 #(#binary_locals)*
2083 loop {
2084 let field = match multipart.next_field().await {
2085 Ok(Some(field)) => field,
2086 Ok(None) => break,
2087 Err(_) => return ::axum::response::IntoResponse::into_response(
2088 (::axum::http::StatusCode::BAD_REQUEST, "malformed multipart request")
2089 ),
2090 };
2091 let name = match field.name() {
2092 Some(name) => name.to_string(),
2093 None => return ::axum::response::IntoResponse::into_response(
2094 (::axum::http::StatusCode::BAD_REQUEST, "multipart field has no name")
2095 ),
2096 };
2097 if object.contains_key(&name) {
2098 return ::axum::response::IntoResponse::into_response(
2099 (::axum::http::StatusCode::BAD_REQUEST, "duplicate multipart field")
2100 );
2101 }
2102 let value = match name.as_str() {
2103 #(#field_arms,)*
2104 _ => return ::axum::response::IntoResponse::into_response(
2105 (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "unknown multipart field")
2106 ),
2107 };
2108 let validation_value = match name.as_str() {
2109 #(#binary_validation_arms)*
2110 _ => value.clone(),
2111 };
2112 object.insert(name.clone(), value);
2113 validation_object.insert(name, validation_value);
2114 }
2115 #multipart_validation
2116 match ::serde_json::from_value::<#body_ty_tokens>(::serde_json::Value::Object(object)) {
2117 Ok(mut body) => {
2118 #(#binary_patches)*
2119 Some(body)
2120 },
2121 Err(_) => return ::axum::response::IntoResponse::into_response(
2122 (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "invalid multipart body")
2123 ),
2124 }
2125 };
2126 };
2127 if required {
2128 body_decode.extend(quote! {
2129 let body = match body {
2130 Some(body) => body,
2131 None => return ::axum::response::IntoResponse::into_response(
2132 (::axum::http::StatusCode::UNPROCESSABLE_ENTITY, "missing required multipart body")
2133 ),
2134 };
2135 });
2136 }
2137 call_args.push(quote! { body });
2138 } else if validated_json {
2139 extractors.push(quote! { __request: ::axum::extract::Request });
2140 let Some(RequestBodyContent::Json { media_type, .. }) = &op.request_body else {
2141 return Err(ServerCodegenError::Internal(
2142 "validated JSON body lost its media type".to_string(),
2143 ));
2144 };
2145 let target = self
2146 .validation_target(validation_bundle, op, "body", None)?
2147 .ok_or_else(|| {
2148 ServerCodegenError::Validation(format!(
2149 "validation target unexpectedly disabled for operation `{}` body",
2150 op.operation_id
2151 ))
2152 })?;
2153 let required = op.request_body_required;
2154 let max_body_bytes = self.server.validation.max_body_bytes;
2155 body_decode = if required {
2156 quote! {
2157 let body: #body_ty_tokens = match super::validation::decode_json_body::<#body_ty_tokens>(
2158 __request,
2159 #target,
2160 #media_type,
2161 true,
2162 #max_body_bytes,
2163 ).await {
2164 Ok(Some(body)) => body,
2165 Ok(None) => return ::axum::response::IntoResponse::into_response(
2166 super::validation::generated_contract_error()
2167 ),
2168 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
2169 };
2170 }
2171 } else {
2172 quote! {
2173 let body: ::std::option::Option<#body_ty_tokens> =
2174 match super::validation::decode_json_body::<#body_ty_tokens>(
2175 __request,
2176 #target,
2177 #media_type,
2178 false,
2179 #max_body_bytes,
2180 ).await {
2181 Ok(body) => body,
2182 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
2183 };
2184 }
2185 };
2186 call_args.push(quote! { body });
2187 } else if validated_form {
2188 extractors.push(quote! { __request: ::axum::extract::Request });
2189 let Some(RequestBodyContent::FormUrlEncoded { media_type, .. }) = &op.request_body
2190 else {
2191 return Err(ServerCodegenError::Internal(
2192 "validated form body lost its media type".to_string(),
2193 ));
2194 };
2195 let target = self
2196 .validation_target(validation_bundle, op, "body", None)?
2197 .ok_or_else(|| {
2198 ServerCodegenError::Validation(format!(
2199 "validation target unexpectedly disabled for operation `{}` body",
2200 op.operation_id
2201 ))
2202 })?;
2203 let allowed_fields = self.form_field_names(op)?;
2204 let required_fields = self.form_required_field_names(op)?;
2205 let required = op.request_body_required;
2206 let max_body_bytes = self.server.validation.max_body_bytes;
2207 body_decode = quote! {
2208 let body: ::std::option::Option<#body_ty_tokens> =
2209 match super::validation::decode_form_body::<#body_ty_tokens>(
2210 __request,
2211 #target,
2212 #media_type,
2213 #required,
2214 #max_body_bytes,
2215 &[#(#allowed_fields),*],
2216 &[#(#required_fields),*],
2217 ).await {
2218 Ok(body) => body,
2219 Err(rejection) => return ::axum::response::IntoResponse::into_response(rejection),
2220 };
2221 };
2222 if required {
2223 body_decode.extend(quote! {
2224 let body = match body {
2225 Some(body) => body,
2226 None => return ::axum::response::IntoResponse::into_response(
2227 super::validation::generated_contract_error()
2228 ),
2229 };
2230 });
2231 }
2232 call_args.push(quote! { body });
2233 } else if op.request_body_required {
2234 extractors.push(quote! {
2235 ::axum::Json(body): ::axum::Json<#body_ty_tokens>
2236 });
2237 call_args.push(quote! { body });
2238 } else {
2239 extractors.push(quote! {
2240 body: ::std::option::Option<::axum::Json<#body_ty_tokens>>
2241 });
2242 call_args.push(quote! { body.map(|::axum::Json(b)| b) });
2243 }
2244 }
2245
2246 let _ = trait_ident;
2250 let raw_query_validation = (!raw_query_validation_checks.is_empty()).then(|| {
2251 quote! {
2252 if let Some(raw) = __raw_query.as_deref() {
2253 if __validate_urlencoded(raw).is_err() {
2254 return ::axum::response::IntoResponse::into_response(
2255 super::validation::malformed_parameter("/query")
2256 );
2257 }
2258 }
2259 let __raw_query_pairs = __query_pairs(__raw_query.as_deref());
2260 #(#raw_query_validation_checks)*
2261 }
2262 });
2263
2264 Ok(quote! {
2269 async fn #handler_ident<T>(
2270 #(#extractors),*
2271 ) -> ::axum::response::Response
2272 where
2273 T: super::api::#trait_ident + Clone + Send + Sync + 'static,
2274 {
2275 #path_decode
2276 #raw_query_validation
2277 #query_decode
2278 #(#required_query_checks)*
2279 #(#query_validation_checks)*
2280 #(#parameter_decode_checks)*
2281 #body_decode
2282 ::axum::response::IntoResponse::into_response(
2283 api.#trait_method(#(#call_args),*).await,
2284 )
2285 }
2286 })
2287 }
2288
2289 fn emit_api(
2290 &self,
2291 groups: &BTreeMap<String, Vec<&OperationInfo>>,
2292 response_enum_names: &BTreeMap<String, syn::Ident>,
2293 ) -> TokenStream {
2294 let provenance_attribute = self.provenance_attribute();
2295 let traits: Vec<TokenStream> = groups
2296 .iter()
2297 .map(|(tag, ops)| self.emit_trait(tag, ops, response_enum_names))
2298 .collect();
2299
2300 let mut emitted: std::collections::BTreeSet<String> = Default::default();
2306 let mut param_enums: Vec<TokenStream> = Vec::new();
2307 for op in groups.values().flatten() {
2308 for p in &op.parameters {
2309 if let Some(values) = &p.enum_values {
2310 if emitted.insert(p.rust_type.clone()) {
2311 param_enums.push(emit_param_enum(&p.rust_type, values));
2312 }
2313 }
2314 }
2315 }
2316
2317 quote! {
2318 #provenance_attribute
2322
2323 #![allow(clippy::too_many_arguments)]
2324
2325 use super::errors::*;
2326 #[allow(unused_imports)]
2331 use super::super::types::*;
2332
2333 #(#param_enums)*
2334
2335 #(#traits)*
2336 }
2337 }
2338
2339 fn emit_trait(
2340 &self,
2341 tag: &str,
2342 ops: &[&OperationInfo],
2343 response_enum_names: &BTreeMap<String, syn::Ident>,
2344 ) -> TokenStream {
2345 let trait_ident = trait_ident_for_tag(tag);
2346 let methods: Vec<TokenStream> = ops
2347 .iter()
2348 .map(|op| self.emit_method_sig(op, response_enum_names))
2349 .collect();
2350 let doc = format!(" Operations under the `{tag}` tag.");
2351 quote! {
2352 #[doc = #doc]
2353 #[async_trait::async_trait]
2354 pub trait #trait_ident: Send + Sync + 'static {
2355 #(#methods)*
2356 }
2357 }
2358 }
2359
2360 fn emit_method_sig(
2361 &self,
2362 op: &OperationInfo,
2363 response_enum_names: &BTreeMap<String, syn::Ident>,
2364 ) -> TokenStream {
2365 let name = format_ident!("{}", op.operation_id.to_snake_case());
2366 let response_ty = &response_enum_names[&op.operation_id];
2367
2368 let mut params: Vec<TokenStream> = Vec::new();
2373 for p in &op.parameters {
2374 if p.location == "path" {
2375 let ident = self.parameter_ident(p);
2376 let ty = self.query_parameter_type(p);
2377 params.push(quote! { #ident: #ty });
2378 }
2379 }
2380 for p in &op.parameters {
2381 if p.location == "query" {
2382 let ident = self.parameter_ident(p);
2383 let ty = self.query_parameter_type(p);
2384 if p.required {
2389 params.push(quote! { #ident: #ty });
2390 } else {
2391 params.push(quote! { #ident: ::std::option::Option<#ty> });
2392 }
2393 }
2394 }
2395 for p in &op.parameters {
2396 if p.location == "header" {
2397 let ident = self.parameter_ident(p);
2398 let ty = self.query_parameter_type(p);
2399 if p.required {
2400 params.push(quote! { #ident: #ty });
2401 } else {
2402 params.push(quote! { #ident: ::std::option::Option<#ty> });
2403 }
2404 }
2405 }
2406 for p in &op.parameters {
2407 if p.location == "cookie" {
2408 let ident = self.parameter_ident(p);
2409 let ty = self.query_parameter_type(p);
2410 if p.required {
2411 params.push(quote! { #ident: #ty });
2412 } else {
2413 params.push(quote! { #ident: ::std::option::Option<#ty> });
2414 }
2415 }
2416 }
2417 if let Some(body) = body_type(op) {
2418 let body_ty = self.model_type(&body);
2419 if op.request_body_required {
2420 params.push(quote! { body: #body_ty });
2421 } else {
2422 params.push(quote! { body: Option<#body_ty> });
2423 }
2424 }
2425
2426 let summary_doc = op
2427 .summary
2428 .as_deref()
2429 .map(|s| format!(" {s}"))
2430 .unwrap_or_default();
2431 let route_doc = format!(" `{} {}`", op.method, op.path);
2432
2433 quote! {
2434 #[doc = #summary_doc]
2435 #[doc = ""]
2436 #[doc = #route_doc]
2437 async fn #name(&self, #(#params),*) -> #response_ty;
2438 }
2439 }
2440
2441 fn emit_query_struct(&self, op: &OperationInfo) -> Option<TokenStream> {
2445 let query_params: Vec<&_> = op
2446 .parameters
2447 .iter()
2448 .filter(|p| p.location == "query")
2449 .collect();
2450 if query_params.is_empty() {
2451 return None;
2452 }
2453 let ident = format_ident!("{}Query", op.operation_id.to_pascal_case());
2454 let decode_ident = format_ident!("__decode_{}_query", op.operation_id.to_snake_case());
2455 let mut fields = Vec::new();
2456 let mut decoders = Vec::new();
2457 let mut field_idents = Vec::new();
2458 for parameter in query_params {
2459 let field_ident = self.parameter_ident(parameter);
2460 let field_type = self.query_parameter_type(parameter);
2461 let wire_name = parameter.name.as_str();
2462 fields.push(quote! {
2463 pub #field_ident: ::std::option::Option<#field_type>
2464 });
2465 field_idents.push(field_ident.clone());
2466
2467 let decoder = match ¶meter.query_serialization {
2468 Some(QuerySerialization::FormExplodedArray { item_type }) => {
2469 if let crate::analysis::ArrayItemType::FlatStructRef { properties, .. } =
2470 item_type
2471 {
2472 let property_names = properties
2473 .iter()
2474 .map(|property| property.wire_name.clone())
2475 .collect::<Vec<_>>();
2476 quote! {
2480 let #field_ident = {
2481 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2482 let prefix = concat!(#wire_name, ".");
2483 let mut groups: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2484 let allowed = [#(#property_names),*];
2485 for (key, value) in &__pairs {
2486 let Some(rest) = key.strip_prefix(prefix) else { continue };
2487 if let Some(index) = rest.strip_suffix("[]").and_then(|index| index.parse::<usize>().ok()) {
2488 groups.entry(index).or_default();
2489 continue;
2490 }
2491 let Some((index, property)) = rest.split_once('.') else { continue };
2492 let Ok(index) = index.parse::<usize>() else { continue };
2493 if !allowed.contains(&property) {
2494 continue;
2495 }
2496 groups
2497 .entry(index)
2498 .or_default()
2499 .insert(property.to_string(), ::serde_json::Value::String(value.clone()));
2500 }
2501 if empty_marker && !groups.is_empty() {
2502 return Err(format!(
2503 "query array `{}` cannot combine values with its empty marker",
2504 #wire_name,
2505 ));
2506 }
2507 if empty_marker {
2508 Some(Vec::new())
2509 } else if groups.is_empty() {
2510 None
2511 } else {
2512 let mut values = Vec::with_capacity(groups.len());
2513 for (_, object) in groups {
2514 values.push(
2515 ::serde_json::from_value(::serde_json::Value::Object(object))
2516 .map_err(|error| format!("invalid query structure for `{}`: {error}", #wire_name))?,
2517 );
2518 }
2519 Some(values)
2520 }
2521 };
2522 }
2523 } else if let crate::analysis::ArrayItemType::NestedStructRef {
2524 properties,
2525 ..
2526 } = item_type
2527 {
2528 let scalar_json = |kind: crate::analysis::QueryScalarType| match kind {
2529 crate::analysis::QueryScalarType::String => {
2530 quote! { ::serde_json::Value::String(value.clone()) }
2531 }
2532 crate::analysis::QueryScalarType::Boolean => quote! {
2533 ::serde_json::Value::Bool(value.parse::<bool>().map_err(|error| {
2534 format!("invalid boolean query value `{value}`: {error}")
2535 })?)
2536 },
2537 crate::analysis::QueryScalarType::Integer
2538 | crate::analysis::QueryScalarType::Number => quote! {
2539 match ::serde_json::from_str::<::serde_json::Value>(value)
2540 .map_err(|error| format!("invalid numeric query value `{value}`: {error}"))?
2541 {
2542 parsed @ ::serde_json::Value::Number(_) => parsed,
2543 _ => return Err(format!("invalid numeric query value `{value}`")),
2544 }
2545 },
2546 };
2547 let property_decoders = properties
2548 .iter()
2549 .enumerate()
2550 .map(|(property_index, property)| {
2551 let property_name = property.wire_name.as_str();
2552 match &property.value_type {
2553 crate::analysis::QueryStructPropertyType::Scalar(kind) => {
2554 let parsed = scalar_json(*kind);
2555 quote! {
2556 for (key, value) in &__pairs {
2557 let Some(rest) = key.strip_prefix(prefix) else { continue };
2558 let Some((index, tail)) = rest.split_once('.') else { continue };
2559 let Ok(index) = index.parse::<usize>() else { continue };
2560 if tail != #property_name { continue; }
2561 let parsed = #parsed;
2562 groups.entry(index).or_default()
2563 .insert(#property_name.to_string(), parsed);
2564 }
2565 }
2566 }
2567 crate::analysis::QueryStructPropertyType::Object { properties } => {
2568 let nested_groups = format_ident!("nested_objects_{property_index}");
2569 let leaf_parsers = properties.iter().map(|leaf_property| {
2570 let leaf = leaf_property.wire_name.as_str();
2571 let crate::analysis::QueryStructPropertyType::Scalar(kind) = leaf_property.value_type else {
2572 unreachable!("flat query object cannot contain nested values");
2573 };
2574 let parsed = scalar_json(kind);
2575 quote! { #leaf => #parsed, }
2576 }).collect::<Vec<_>>();
2577 quote! {
2578 let mut #nested_groups: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2579 for (key, value) in &__pairs {
2580 let Some(rest) = key.strip_prefix(prefix) else { continue };
2581 let Some((index, tail)) = rest.split_once('.') else { continue };
2582 let Ok(index) = index.parse::<usize>() else { continue };
2583 if tail == concat!(#property_name, "[]") {
2584 groups.entry(index).or_default().insert(
2585 #property_name.to_string(),
2586 ::serde_json::Value::Object(::serde_json::Map::new()),
2587 );
2588 continue;
2589 }
2590 let Some(leaf) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2591 let parsed = match leaf { #(#leaf_parsers)* _ => continue };
2592 #nested_groups.entry(index).or_default().insert(leaf.to_string(), parsed);
2593 }
2594 for (index, value) in #nested_groups {
2595 groups.entry(index).or_default().insert(#property_name.to_string(), ::serde_json::Value::Object(value));
2596 }
2597 }
2598 }
2599 crate::analysis::QueryStructPropertyType::Array { item_type } => {
2600 let nested_groups = format_ident!("nested_groups_{property_index}");
2601 match item_type {
2602 crate::analysis::ArrayItemType::Scalar(rust_type) => {
2603 let kind = if rust_type == "String" {
2604 crate::analysis::QueryScalarType::String
2605 } else if rust_type == "bool" {
2606 crate::analysis::QueryScalarType::Boolean
2607 } else if rust_type.starts_with('i') || rust_type.starts_with('u') {
2608 crate::analysis::QueryScalarType::Integer
2609 } else {
2610 crate::analysis::QueryScalarType::Number
2611 };
2612 let parsed = scalar_json(kind);
2613 quote! {
2614 let mut #nested_groups: ::std::collections::BTreeMap<usize, ::std::collections::BTreeMap<usize, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2615 for (key, value) in &__pairs {
2616 let Some(rest) = key.strip_prefix(prefix) else { continue };
2617 let Some((index, tail)) = rest.split_once('.') else { continue };
2618 let Ok(index) = index.parse::<usize>() else { continue };
2619 if tail == concat!(#property_name, "[]") {
2620 groups.entry(index).or_default().insert(
2621 #property_name.to_string(),
2622 ::serde_json::Value::Array(Vec::new()),
2623 );
2624 continue;
2625 }
2626 let Some(nested_index) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2627 let Ok(nested_index) = nested_index.parse::<usize>() else { continue };
2628 let parsed = #parsed;
2629 #nested_groups.entry(index).or_default().insert(nested_index, parsed);
2630 }
2631 for (index, values) in #nested_groups {
2632 groups.entry(index).or_default().insert(
2633 #property_name.to_string(),
2634 ::serde_json::Value::Array(values.into_values().collect()),
2635 );
2636 }
2637 }
2638 }
2639 crate::analysis::ArrayItemType::SchemaRef(_) => quote! {
2640 let mut #nested_groups: ::std::collections::BTreeMap<usize, ::std::collections::BTreeMap<usize, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2641 for (key, value) in &__pairs {
2642 let Some(rest) = key.strip_prefix(prefix) else { continue };
2643 let Some((index, tail)) = rest.split_once('.') else { continue };
2644 let Ok(index) = index.parse::<usize>() else { continue };
2645 if tail == concat!(#property_name, "[]") {
2646 groups.entry(index).or_default().insert(
2647 #property_name.to_string(),
2648 ::serde_json::Value::Array(Vec::new()),
2649 );
2650 continue;
2651 }
2652 let Some(nested_index) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2653 let Ok(nested_index) = nested_index.parse::<usize>() else { continue };
2654 #nested_groups.entry(index).or_default().insert(
2655 nested_index,
2656 ::serde_json::Value::String(value.clone()),
2657 );
2658 }
2659 for (index, values) in #nested_groups {
2660 groups.entry(index).or_default().insert(
2661 #property_name.to_string(),
2662 ::serde_json::Value::Array(values.into_values().collect()),
2663 );
2664 }
2665 },
2666 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
2667 let allowed = properties.iter().map(|property| property.wire_name.clone()).collect::<Vec<_>>();
2668 let leaf_kinds = properties.iter().map(|property| match property.value_type {
2669 crate::analysis::QueryStructPropertyType::Scalar(kind) => kind,
2670 crate::analysis::QueryStructPropertyType::Array { .. }
2671 | crate::analysis::QueryStructPropertyType::Object { .. } => unreachable!("flat query struct cannot contain nested values"),
2672 }).collect::<Vec<_>>();
2673 let leaf_parsers = allowed.iter().zip(leaf_kinds).map(|(leaf, kind)| {
2674 let parsed = scalar_json(kind);
2675 quote! {
2676 #leaf => #parsed,
2677 }
2678 }).collect::<Vec<_>>();
2679 quote! {
2680 let mut #nested_groups: ::std::collections::BTreeMap<usize, ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>>> = ::std::collections::BTreeMap::new();
2681 for (key, value) in &__pairs {
2682 let Some(rest) = key.strip_prefix(prefix) else { continue };
2683 let Some((index, tail)) = rest.split_once('.') else { continue };
2684 let Ok(index) = index.parse::<usize>() else { continue };
2685 if tail == concat!(#property_name, "[]") {
2686 groups.entry(index).or_default().insert(
2687 #property_name.to_string(),
2688 ::serde_json::Value::Array(Vec::new()),
2689 );
2690 continue;
2691 }
2692 let Some(tail) = tail.strip_prefix(concat!(#property_name, ".")) else { continue };
2693 if let Some(nested_index) = tail.strip_suffix("[]").and_then(|index| index.parse::<usize>().ok()) {
2694 #nested_groups.entry(index).or_default().entry(nested_index).or_default();
2695 continue;
2696 }
2697 let Some((nested_index, leaf)) = tail.split_once('.') else { continue };
2698 let Ok(nested_index) = nested_index.parse::<usize>() else { continue };
2699 let parsed = match leaf {
2700 #(#leaf_parsers)*
2701 _ => continue,
2702 };
2703 #nested_groups.entry(index).or_default()
2704 .entry(nested_index).or_default()
2705 .insert(leaf.to_string(), parsed);
2706 }
2707 for (index, values) in #nested_groups {
2708 groups.entry(index).or_default().insert(
2709 #property_name.to_string(),
2710 ::serde_json::Value::Array(values.into_values().map(::serde_json::Value::Object).collect()),
2711 );
2712 }
2713 }
2714 }
2715 crate::analysis::ArrayItemType::NestedStructRef { .. } => unreachable!("analysis rejects query nesting deeper than two levels"),
2716 }
2717 }
2718 }
2719 })
2720 .collect::<Vec<_>>();
2721 quote! {
2722 let #field_ident = {
2723 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2724 let prefix = concat!(#wire_name, ".");
2725 let mut groups: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2726 for (key, _) in &__pairs {
2727 let Some(rest) = key.strip_prefix(prefix) else { continue };
2728 if let Some(index) = rest.strip_suffix("[]").and_then(|index| index.parse::<usize>().ok()) {
2729 groups.entry(index).or_default();
2730 }
2731 }
2732 #(#property_decoders)*
2733 if empty_marker && !groups.is_empty() {
2734 return Err(format!(
2735 "query array `{}` cannot combine values with its empty marker",
2736 #wire_name,
2737 ));
2738 }
2739 if empty_marker {
2740 Some(Vec::new())
2741 } else if groups.is_empty() {
2742 None
2743 } else {
2744 let mut values = Vec::with_capacity(groups.len());
2745 for (_, object) in groups {
2746 values.push(
2747 ::serde_json::from_value(::serde_json::Value::Object(object))
2748 .map_err(|error| format!("invalid nested query structure for `{}`: {error}", #wire_name))?,
2749 );
2750 }
2751 Some(values)
2752 }
2753 };
2754 }
2755 } else {
2756 quote! {
2757 let #field_ident = {
2758 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2759 let raw_values: Vec<&str> = __pairs
2760 .iter()
2761 .filter(|(key, _)| key == #wire_name)
2762 .map(|(_, value)| value.as_str())
2763 .collect();
2764 if empty_marker && !raw_values.is_empty() {
2765 return Err(format!(
2766 "query array `{}` cannot combine values with its empty marker",
2767 #wire_name,
2768 ));
2769 }
2770 if empty_marker {
2771 Some(Vec::new())
2772 } else if raw_values.is_empty() {
2773 None
2774 } else {
2775 let mut values = Vec::with_capacity(raw_values.len());
2776 for raw in raw_values {
2777 values.push(__decode_query_scalar(raw, #wire_name)?);
2778 }
2779 Some(values)
2780 }
2781 };
2782 }
2783 }
2784 }
2785 Some(QuerySerialization::FormArray { .. }) => quote! {
2786 let #field_ident = match (
2787 __query_one(&__pairs, #wire_name)?,
2788 __query_empty_marker(&__pairs, #wire_name)?,
2789 ) {
2790 (Some(_), true) => return Err(format!(
2791 "query array `{}` cannot combine a value with its empty marker",
2792 #wire_name,
2793 )),
2794 (Some(raw), false) => {
2795 let mut values = Vec::new();
2796 for item in raw.split(',') {
2797 values.push(__decode_query_scalar(item, #wire_name)?);
2798 }
2799 Some(values)
2800 }
2801 (None, true) => Some(Vec::new()),
2802 (None, false) => None,
2803 };
2804 },
2805 Some(QuerySerialization::FormExplodedNestedObject { properties }) => {
2806 let scalar_json = |kind: crate::analysis::QueryScalarType| match kind {
2807 crate::analysis::QueryScalarType::String => {
2808 quote! { ::serde_json::Value::String(value.clone()) }
2809 }
2810 crate::analysis::QueryScalarType::Boolean => quote! {
2811 ::serde_json::Value::Bool(value.parse::<bool>().map_err(|error| format!("invalid boolean query value `{value}`: {error}"))?)
2812 },
2813 crate::analysis::QueryScalarType::Integer
2814 | crate::analysis::QueryScalarType::Number => quote! {
2815 match ::serde_json::from_str::<::serde_json::Value>(value)
2816 .map_err(|error| format!("invalid numeric query value `{value}`: {error}"))?
2817 {
2818 parsed @ ::serde_json::Value::Number(_) => parsed,
2819 _ => return Err(format!("invalid numeric query value `{value}`")),
2820 }
2821 },
2822 };
2823 let property_decoders = properties.iter().enumerate().map(|(property_index, property)| {
2824 let property_name = property.wire_name.as_str();
2825 match &property.value_type {
2826 crate::analysis::QueryStructPropertyType::Scalar(kind) => {
2827 let parsed = scalar_json(*kind);
2828 quote! {
2829 if let Some((_, value)) = __pairs.iter().find(|(key, _)| key == concat!(#wire_name, ".", #property_name)) {
2830 let parsed = #parsed;
2831 object.insert(#property_name.to_string(), parsed);
2832 }
2833 }
2834 }
2835 crate::analysis::QueryStructPropertyType::Object { properties } => {
2836 let property_wire_name = format!("{wire_name}.{property_name}");
2837 let leaf_parsers = properties.iter().map(|leaf_property| {
2838 let leaf = leaf_property.wire_name.as_str();
2839 let crate::analysis::QueryStructPropertyType::Scalar(kind) = leaf_property.value_type else {
2840 unreachable!("flat query object cannot contain nested values");
2841 };
2842 let parsed = scalar_json(kind);
2843 quote! { #leaf => #parsed, }
2844 }).collect::<Vec<_>>();
2845 quote! {
2846 let mut nested = ::serde_json::Map::new();
2847 for (key, value) in &__pairs {
2848 let Some(leaf) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2849 let parsed = match leaf { #(#leaf_parsers)* _ => continue };
2850 nested.insert(leaf.to_string(), parsed);
2851 }
2852 let nested_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2853 if nested_empty_marker && !nested.is_empty() {
2854 return Err(format!("query object `{}` cannot combine properties with its empty marker", #property_wire_name));
2855 }
2856 if nested_empty_marker || !nested.is_empty() {
2857 object.insert(#property_name.to_string(), ::serde_json::Value::Object(nested));
2858 }
2859 }
2860 }
2861 crate::analysis::QueryStructPropertyType::Array { item_type } => {
2862 let values_ident = format_ident!("property_values_{property_index}");
2863 let property_wire_name = format!("{wire_name}.{property_name}");
2864 match item_type {
2865 crate::analysis::ArrayItemType::Scalar(rust_type) => {
2866 let kind = if rust_type == "String" {
2867 crate::analysis::QueryScalarType::String
2868 } else if rust_type == "bool" {
2869 crate::analysis::QueryScalarType::Boolean
2870 } else if rust_type.starts_with('i') || rust_type.starts_with('u') {
2871 crate::analysis::QueryScalarType::Integer
2872 } else {
2873 crate::analysis::QueryScalarType::Number
2874 };
2875 let parsed = scalar_json(kind);
2876 quote! {
2877 let mut #values_ident: ::std::collections::BTreeMap<usize, ::serde_json::Value> = ::std::collections::BTreeMap::new();
2878 for (key, value) in &__pairs {
2879 let Some(index) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2880 let Ok(index) = index.parse::<usize>() else { continue };
2881 let parsed = #parsed;
2882 #values_ident.insert(index, parsed);
2883 }
2884 let property_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2885 if property_empty_marker && !#values_ident.is_empty() {
2886 return Err(format!("query array `{}` cannot combine values with its empty marker", #property_wire_name));
2887 }
2888 if property_empty_marker || !#values_ident.is_empty() {
2889 object.insert(#property_name.to_string(), ::serde_json::Value::Array(#values_ident.into_values().collect()));
2890 }
2891 }
2892 }
2893 crate::analysis::ArrayItemType::SchemaRef(_) => quote! {
2894 let mut #values_ident: ::std::collections::BTreeMap<usize, ::serde_json::Value> = ::std::collections::BTreeMap::new();
2895 for (key, value) in &__pairs {
2896 let Some(index) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2897 let Ok(index) = index.parse::<usize>() else { continue };
2898 #values_ident.insert(index, ::serde_json::Value::String(value.clone()));
2899 }
2900 let property_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2901 if property_empty_marker && !#values_ident.is_empty() {
2902 return Err(format!("query array `{}` cannot combine values with its empty marker", #property_wire_name));
2903 }
2904 if property_empty_marker || !#values_ident.is_empty() {
2905 object.insert(#property_name.to_string(), ::serde_json::Value::Array(#values_ident.into_values().collect()));
2906 }
2907 },
2908 crate::analysis::ArrayItemType::FlatStructRef { properties, .. } => {
2909 let leaf_parsers = properties.iter().map(|leaf_property| {
2910 let leaf = leaf_property.wire_name.as_str();
2911 let crate::analysis::QueryStructPropertyType::Scalar(kind) = leaf_property.value_type else {
2912 unreachable!("flat query struct cannot contain arrays");
2913 };
2914 let parsed = scalar_json(kind);
2915 quote! { #leaf => #parsed, }
2916 }).collect::<Vec<_>>();
2917 quote! {
2918 let mut #values_ident: ::std::collections::BTreeMap<usize, ::serde_json::Map<String, ::serde_json::Value>> = ::std::collections::BTreeMap::new();
2919 for (key, value) in &__pairs {
2920 let Some(tail) = key.strip_prefix(concat!(#wire_name, ".", #property_name, ".")) else { continue };
2921 let Some((index, leaf)) = tail.split_once('.') else { continue };
2922 let Ok(index) = index.parse::<usize>() else { continue };
2923 let parsed = match leaf { #(#leaf_parsers)* _ => continue };
2924 #values_ident.entry(index).or_default().insert(leaf.to_string(), parsed);
2925 }
2926 let property_empty_marker = __query_empty_marker(&__pairs, #property_wire_name)?;
2927 if property_empty_marker && !#values_ident.is_empty() {
2928 return Err(format!("query array `{}` cannot combine values with its empty marker", #property_wire_name));
2929 }
2930 if property_empty_marker || !#values_ident.is_empty() {
2931 object.insert(#property_name.to_string(), ::serde_json::Value::Array(
2932 #values_ident.into_values().map(::serde_json::Value::Object).collect()
2933 ));
2934 }
2935 }
2936 }
2937 crate::analysis::ArrayItemType::NestedStructRef { .. } => unreachable!("analysis rejects query nesting deeper than two levels"),
2938 }
2939 }
2940 }
2941 }).collect::<Vec<_>>();
2942 quote! {
2943 let #field_ident = {
2944 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2945 let mut object = ::serde_json::Map::new();
2946 #(#property_decoders)*
2947 if empty_marker && !object.is_empty() {
2948 return Err(format!("query object `{}` cannot combine properties with its empty marker", #wire_name));
2949 }
2950 if object.is_empty() && !empty_marker {
2951 None
2952 } else {
2953 Some(::serde_json::from_value(::serde_json::Value::Object(object))
2954 .map_err(|error| format!("invalid nested query object for `{}`: {error}", #wire_name))?)
2955 }
2956 };
2957 }
2958 }
2959 Some(QuerySerialization::FormExplodedObject) => {
2960 let property_names = self
2961 .query_object_properties(parameter)
2962 .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
2963 .unwrap_or_default();
2964 let required_names = self.query_object_required_properties(parameter);
2965 quote! {
2966 let #field_ident = {
2967 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
2968 let allowed = [#(#property_names),*];
2969 let object_fields: Vec<(String, String)> = __pairs
2970 .iter()
2971 .filter(|(key, _)| allowed.contains(&key.as_str()))
2972 .cloned()
2973 .collect();
2974 if empty_marker && !object_fields.is_empty() {
2975 return Err(format!(
2976 "query object `{}` cannot combine properties with its empty marker",
2977 #wire_name,
2978 ));
2979 }
2980 if object_fields.is_empty() && !empty_marker {
2981 None
2982 } else {
2983 let required_properties: &[&str] = &[#(#required_names),*];
2984 for required in required_properties {
2985 if !object_fields.iter().any(|(key, _)| key == required) {
2986 return Err(format!("query object `{}` is missing a required property", #wire_name));
2987 }
2988 }
2989 Some(__decode_query_object(&object_fields, #wire_name)?)
2990 }
2991 };
2992 }
2993 }
2994 Some(QuerySerialization::FormObject) => {
2995 let property_names = self
2996 .query_object_properties(parameter)
2997 .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
2998 .unwrap_or_default();
2999 let required_names = self.query_object_required_properties(parameter);
3000 let has_required_names = !required_names.is_empty();
3001 quote! {
3002 let #field_ident = match (
3003 __query_one(&__pairs, #wire_name)?,
3004 __query_empty_marker(&__pairs, #wire_name)?,
3005 ) {
3006 (Some(_), true) => return Err(format!(
3007 "query object `{}` cannot combine a value with its empty marker",
3008 #wire_name,
3009 )),
3010 (Some(raw), false) => {
3011 let parts: Vec<&str> = raw.split(',').collect();
3012 if parts.len() % 2 != 0 {
3013 return Err(format!(
3014 "query object `{}` must contain alternating key,value entries",
3015 #wire_name,
3016 ));
3017 }
3018 let allowed = [#(#property_names),*];
3019 let mut seen = ::std::collections::BTreeSet::new();
3020 let object_fields: Vec<(String, String)> = parts
3021 .chunks_exact(2)
3022 .map(|pair| (pair[0].to_string(), pair[1].to_string()))
3023 .collect();
3024 for (key, _) in &object_fields {
3025 if !allowed.contains(&key.as_str()) || !seen.insert(key.as_str()) {
3026 return Err(format!("query object `{}` has invalid properties", #wire_name));
3027 }
3028 }
3029 let required_properties: &[&str] = &[#(#required_names),*];
3030 for required in required_properties {
3031 if !seen.contains(required) {
3032 return Err(format!("query object `{}` is missing a required property", #wire_name));
3033 }
3034 }
3035 Some(__decode_query_object(&object_fields, #wire_name)?)
3036 }
3037 (None, true) if #has_required_names => return Err(format!(
3038 "query object `{}` is missing a required property",
3039 #wire_name,
3040 )),
3041 (None, true) => Some(__decode_query_object(&[], #wire_name)?),
3042 (None, false) => None,
3043 };
3044 }
3045 }
3046 Some(QuerySerialization::DeepObject) => {
3047 let property_names = self
3048 .query_object_properties(parameter)
3049 .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
3050 .unwrap_or_default();
3051 let required_names = self.query_object_required_properties(parameter);
3052 quote! {
3053 let #field_ident = {
3054 let empty_marker = __query_empty_marker(&__pairs, #wire_name)?;
3055 let prefix = format!("{}[", #wire_name);
3056 let allowed = [#(#property_names),*];
3057 let mut object_fields = Vec::new();
3058 for (key, value) in &__pairs {
3059 if let Some(property) = key
3060 .strip_prefix(&prefix)
3061 .and_then(|rest| rest.strip_suffix(']'))
3062 {
3063 if property.is_empty() {
3064 continue;
3065 }
3066 if !allowed.contains(&property) {
3067 return Err(format!(
3068 "unknown deepObject property `{}[{}]`",
3069 #wire_name,
3070 property,
3071 ));
3072 }
3073 object_fields.push((property.to_string(), value.clone()));
3074 }
3075 }
3076 if empty_marker && !object_fields.is_empty() {
3077 return Err(format!(
3078 "query object `{}` cannot combine properties with its empty marker",
3079 #wire_name,
3080 ));
3081 }
3082 if object_fields.is_empty() && !empty_marker {
3083 None
3084 } else {
3085 let required_properties: &[&str] = &[#(#required_names),*];
3086 for required in required_properties {
3087 if !object_fields.iter().any(|(key, _)| key == required) {
3088 return Err(format!("query object `{}` is missing a required property", #wire_name));
3089 }
3090 }
3091 Some(__decode_query_object(&object_fields, #wire_name)?)
3092 }
3093 };
3094 }
3095 }
3096 Some(
3097 QuerySerialization::Unsupported { .. }
3098 | QuerySerialization::SimpleHeaderArray { .. },
3099 )
3100 | None => quote! {
3101 let #field_ident = __query_one(&__pairs, #wire_name)?
3102 .map(|raw| __decode_query_scalar(&raw, #wire_name))
3103 .transpose()?;
3104 },
3105 };
3106 decoders.push(decoder);
3107 }
3108 let doc = format!(
3109 " Query parameters for `{} {}` (operationId `{}`).",
3110 op.method, op.path, op.operation_id
3111 );
3112 Some(quote! {
3113 #[doc = #doc]
3114 #[derive(Debug, Default)]
3115 pub struct #ident {
3116 #(#fields),*
3117 }
3118
3119 fn #decode_ident(
3120 raw: ::std::option::Option<&str>,
3121 ) -> ::std::result::Result<#ident, String> {
3122 if let Some(raw) = raw {
3123 __validate_urlencoded(raw)?;
3124 }
3125 let __pairs = __query_pairs(raw);
3126 #(#decoders)*
3127 Ok(#ident {
3128 #(#field_idents),*
3129 })
3130 }
3131 })
3132 }
3133
3134 fn emit_errors(
3135 &self,
3136 ops: &[&OperationInfo],
3137 validation_enabled: bool,
3138 response_enum_names: &BTreeMap<String, syn::Ident>,
3139 ) -> TokenStream {
3140 let provenance_attribute = self.provenance_attribute();
3141 let any_streaming = ops.iter().any(|op| op.supports_streaming);
3142 let enums: Vec<TokenStream> = ops
3143 .iter()
3144 .map(|op| self.emit_response_enum(op, response_enum_names))
3145 .collect();
3146 let problem_types = validation_enabled.then(|| {
3147 quote! {
3148 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)]
3150 pub struct ProblemDetails {
3151 #[serde(rename = "type")]
3152 pub r#type: String,
3153 pub title: String,
3154 pub status: u16,
3155 pub code: String,
3156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3157 pub errors: Vec<InvalidParameter>,
3158 }
3159
3160 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)]
3162 pub struct InvalidParameter {
3163 pub code: String,
3164 pub location: String,
3165 pub message: String,
3166 }
3167
3168 #[derive(Debug, Clone)]
3170 pub struct RequestValidationRejection(pub ProblemDetails);
3171
3172 impl IntoResponse for RequestValidationRejection {
3173 fn into_response(self) -> ::axum::response::Response {
3174 let status = StatusCode::from_u16(self.0.status)
3175 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
3176 let mut response = (status, Json(self.0)).into_response();
3177 response.headers_mut().insert(
3178 ::axum::http::header::CONTENT_TYPE,
3179 ::axum::http::HeaderValue::from_static("application/problem+json"),
3180 );
3181 response
3182 }
3183 }
3184 }
3185 });
3186
3187 let stream_alias = if any_streaming {
3192 quote! {
3193 pub type ServerEventStream = ::std::pin::Pin<
3196 Box<
3197 dyn ::futures_core::Stream<
3198 Item = ::std::result::Result<
3199 ::axum::response::sse::Event,
3200 ::std::convert::Infallible,
3201 >,
3202 > + ::std::marker::Send
3203 + 'static,
3204 >,
3205 >;
3206
3207 pub fn sse_response<S>(stream: S) -> ::axum::response::sse::Sse<ServerEventStream>
3212 where
3213 S: ::futures_core::Stream<
3214 Item = ::std::result::Result<
3215 ::axum::response::sse::Event,
3216 ::std::convert::Infallible,
3217 >,
3218 > + ::std::marker::Send
3219 + 'static,
3220 {
3221 ::axum::response::sse::Sse::new(Box::pin(stream))
3222 }
3223 }
3224 } else {
3225 quote! {}
3226 };
3227
3228 quote! {
3235 #provenance_attribute
3240
3241 #![allow(clippy::large_enum_variant)]
3242
3243 use axum::{
3244 http::StatusCode,
3245 response::IntoResponse,
3246 Json,
3247 };
3248 #[allow(unused_imports)]
3253 use super::super::types::*;
3254
3255 #problem_types
3256
3257 #stream_alias
3258
3259 #(#enums)*
3260 }
3261 }
3262
3263 fn emit_response_enum(
3264 &self,
3265 op: &OperationInfo,
3266 response_enum_names: &BTreeMap<String, syn::Ident>,
3267 ) -> TokenStream {
3268 let enum_ident = &response_enum_names[&op.operation_id];
3269 let mut variants: Vec<TokenStream> = Vec::new();
3270 let mut arms: Vec<TokenStream> = Vec::new();
3271
3272 let fallback_responses;
3277 let responses = if let Some(responses) = self
3278 .analysis
3279 .operation_responses
3280 .get(&op.operation_id)
3281 .filter(|responses| !responses.is_empty())
3282 {
3283 responses
3284 } else {
3285 fallback_responses = op
3286 .response_schemas
3287 .iter()
3288 .map(|(status, schema_name)| {
3289 (
3290 status.clone(),
3291 OperationResponse {
3292 schema_name: Some(schema_name.clone()),
3293 media_type: Some("application/json".to_string()),
3294 body: Some(OperationResponseBody::Json {
3295 schema_name: schema_name.clone(),
3296 media_type: "application/json".to_string(),
3297 }),
3298 supports_streaming: false,
3299 has_content: true,
3300 unsupported_media_types: Vec::new(),
3301 representations: BTreeMap::new(),
3302 },
3303 )
3304 })
3305 .collect::<BTreeMap<_, _>>();
3306 &fallback_responses
3307 };
3308
3309 for (status, response) in responses {
3310 let base_name = status_variant_name(status);
3311 let variant = format_ident!("{}", base_name);
3312 let runtime_status = response_uses_runtime_status(status);
3313 let status_expr = status_token(status);
3314 let status_guard = runtime_status_guard(status, quote! { status });
3315
3316 let buffered_body = response.body.clone().or_else(|| {
3317 response
3318 .schema_name
3319 .as_ref()
3320 .map(|schema_name| OperationResponseBody::Json {
3321 schema_name: schema_name.clone(),
3322 media_type: response
3323 .media_type
3324 .clone()
3325 .unwrap_or_else(|| "application/json".to_string()),
3326 })
3327 });
3328
3329 if let Some(body) = buffered_body {
3330 let (body_ty, response_body, media_type, wildcard) = match body {
3331 OperationResponseBody::Json {
3332 schema_name,
3333 media_type,
3334 } => (
3335 self.model_type(&schema_name),
3336 quote! { Json(body) },
3337 media_type,
3338 false,
3339 ),
3340 OperationResponseBody::Text { media_type } => {
3341 (quote! { String }, quote! { body }, media_type, false)
3342 }
3343 OperationResponseBody::Binary {
3344 media_type,
3345 wildcard,
3346 } => (
3347 quote! { bytes::Bytes },
3348 quote! { body },
3349 media_type,
3350 wildcard,
3351 ),
3352 };
3353 if wildcard {
3354 if runtime_status {
3355 variants.push(quote! {
3356 #variant(StatusCode, ::axum::http::HeaderValue, #body_ty)
3357 });
3358 arms.push(quote! {
3359 Self::#variant(status, content_type, body) => {
3360 if !(#status_guard) {
3361 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3362 }
3363 let mut response = (status, #response_body).into_response();
3364 response.headers_mut().insert(
3365 ::axum::http::header::CONTENT_TYPE,
3366 content_type,
3367 );
3368 response
3369 }
3370 });
3371 } else {
3372 variants.push(quote! {
3373 #variant(::axum::http::HeaderValue, #body_ty)
3374 });
3375 arms.push(quote! {
3376 Self::#variant(content_type, body) => {
3377 let mut response = (#status_expr, #response_body).into_response();
3378 response.headers_mut().insert(
3379 ::axum::http::header::CONTENT_TYPE,
3380 content_type,
3381 );
3382 response
3383 }
3384 });
3385 }
3386 } else if runtime_status {
3387 variants.push(quote! { #variant(StatusCode, #body_ty) });
3388 arms.push(quote! {
3389 Self::#variant(status, body) => {
3390 if !(#status_guard) {
3391 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3392 }
3393 let mut response = (status, #response_body).into_response();
3394 let Ok(content_type) = ::axum::http::HeaderValue::from_bytes(#media_type.as_bytes()) else {
3395 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3396 };
3397 response.headers_mut().insert(
3398 ::axum::http::header::CONTENT_TYPE,
3399 content_type,
3400 );
3401 response
3402 }
3403 });
3404 } else {
3405 variants.push(quote! { #variant(#body_ty) });
3406 arms.push(quote! {
3407 Self::#variant(body) => {
3408 let mut response = (#status_expr, #response_body).into_response();
3409 let Ok(content_type) = ::axum::http::HeaderValue::from_bytes(#media_type.as_bytes()) else {
3410 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3411 };
3412 response.headers_mut().insert(
3413 ::axum::http::header::CONTENT_TYPE,
3414 content_type,
3415 );
3416 response
3417 }
3418 });
3419 }
3420 } else if !response.has_content {
3421 if runtime_status {
3422 variants.push(quote! { #variant(StatusCode) });
3423 arms.push(quote! {
3424 Self::#variant(status) => {
3425 if #status_guard {
3426 status.into_response()
3427 } else {
3428 StatusCode::INTERNAL_SERVER_ERROR.into_response()
3429 }
3430 }
3431 });
3432 } else {
3433 variants.push(quote! { #variant });
3434 arms.push(quote! {
3435 Self::#variant => #status_expr.into_response()
3436 });
3437 }
3438 }
3439
3440 if response.supports_streaming {
3443 let stream_variant = format_ident!("{}Stream", base_name);
3444 if runtime_status {
3445 variants.push(quote! {
3446 #stream_variant(StatusCode, ::axum::response::sse::Sse<ServerEventStream>)
3447 });
3448 arms.push(quote! {
3449 Self::#stream_variant(status, sse) => {
3450 if !(#status_guard) {
3451 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
3452 }
3453 let mut response = sse.into_response();
3454 *response.status_mut() = status;
3455 response
3456 }
3457 });
3458 } else {
3459 variants.push(quote! {
3460 #stream_variant(::axum::response::sse::Sse<ServerEventStream>)
3461 });
3462 arms.push(quote! {
3463 Self::#stream_variant(sse) => {
3464 let mut response = sse.into_response();
3465 *response.status_mut() = #status_expr;
3466 response
3467 }
3468 });
3469 }
3470 }
3471 }
3472
3473 if variants.is_empty() {
3477 variants.push(quote! { Empty });
3478 arms.push(quote! {
3479 Self::Empty => StatusCode::NO_CONTENT.into_response()
3480 });
3481 }
3482
3483 let doc = format!(
3484 " Response for `{} {}` (operationId `{}`).",
3485 op.method, op.path, op.operation_id
3486 );
3487
3488 quote! {
3489 #[doc = #doc]
3490 pub enum #enum_ident {
3491 #(#variants),*
3492 }
3493
3494 impl IntoResponse for #enum_ident {
3495 fn into_response(self) -> ::axum::response::Response {
3496 match self {
3497 #(#arms),*
3498 }
3499 }
3500 }
3501 }
3502 }
3503}
3504
3505fn parameter_location(location: &str, name: &str) -> String {
3506 format!("/{location}/{}", name.replace('~', "~0").replace('/', "~1"))
3507}
3508
3509fn emit_param_enum(name: &str, values: &[String]) -> TokenStream {
3514 let enum_ident = format_ident!("{}", name);
3515 let variants: Vec<TokenStream> = values
3516 .iter()
3517 .enumerate()
3518 .map(|(i, raw)| {
3519 let pascal = raw.to_pascal_case();
3520 let starts_with_digit = pascal
3525 .chars()
3526 .next()
3527 .map(|c| c.is_ascii_digit())
3528 .unwrap_or(true);
3529 let v_name = if pascal.is_empty() || starts_with_digit {
3530 format!("Variant{i}")
3531 } else {
3532 pascal
3533 };
3534 let v_ident = format_ident!("{}", v_name);
3535 let default_marker = if i == 0 {
3536 quote! { #[default] }
3537 } else {
3538 quote! {}
3539 };
3540 quote! {
3541 #default_marker
3542 #[serde(rename = #raw)]
3543 #v_ident
3544 }
3545 })
3546 .collect();
3547 quote! {
3548 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Deserialize, ::serde::Serialize, Default)]
3549 pub enum #enum_ident {
3550 #(#variants),*
3551 }
3552 }
3553}
3554
3555fn axum_method_call(method: &str) -> Option<TokenStream> {
3556 match method.to_ascii_uppercase().as_str() {
3557 "CONNECT" => Some(quote! { connect }),
3558 "DELETE" => Some(quote! { delete }),
3559 "GET" => Some(quote! { get }),
3560 "HEAD" => Some(quote! { head }),
3561 "OPTIONS" => Some(quote! { options }),
3562 "PATCH" => Some(quote! { patch }),
3563 "POST" => Some(quote! { post }),
3564 "PUT" => Some(quote! { put }),
3565 "TRACE" => Some(quote! { trace }),
3566 _ => None,
3567 }
3568}
3569
3570fn axum_custom_route(
3577 path: &str,
3578 dispatcher: &syn::Ident,
3579 methods: &[(String, syn::Ident)],
3580 trait_ident: &syn::Ident,
3581) -> (TokenStream, TokenStream) {
3582 let arms = methods.iter().map(|(method, handler)| {
3583 quote! {
3584 #method => ::axum::handler::Handler::call(#handler::<T>, request, api).await
3585 }
3586 });
3587 let route = quote! {
3588 .route(#path, ::axum::routing::any(#dispatcher::<T>))
3589 };
3590 let dispatcher_fn = quote! {
3591 async fn #dispatcher<T>(
3592 ::axum::extract::State(api): ::axum::extract::State<T>,
3593 request: ::axum::extract::Request,
3594 ) -> ::axum::response::Response
3595 where
3596 T: #trait_ident + Clone + Send + Sync + 'static,
3597 {
3598 match request.method().as_str() {
3599 #(#arms,)*
3600 _ => ::axum::response::IntoResponse::into_response(
3601 ::axum::http::StatusCode::METHOD_NOT_ALLOWED,
3602 ),
3603 }
3604 }
3605 };
3606 (route, dispatcher_fn)
3607}
3608
3609fn path_parameter_affixes<'a>(path: &'a str, parameter_name: &str) -> Option<(&'a str, &'a str)> {
3610 let marker = format!("{{{parameter_name}}}");
3611 for segment in path.split('/') {
3612 let Some(start) = segment.find(&marker) else {
3613 continue;
3614 };
3615 let prefix = &segment[..start];
3616 let suffix = &segment[start + marker.len()..];
3617 if prefix.is_empty() && suffix.is_empty() {
3618 return None;
3619 }
3620 return Some((prefix, suffix));
3621 }
3622 None
3623}
3624
3625fn openapi_to_axum_path(path: &str) -> Result<String, ServerCodegenError> {
3632 let invalid = |reason: &str| ServerCodegenError::InvalidRoutePath {
3633 path: path.to_string(),
3634 reason: reason.to_string(),
3635 };
3636
3637 if !path.starts_with('/') {
3638 return Err(invalid("paths must start with `/`"));
3639 }
3640 let mut route_segments = Vec::new();
3641 for segment in path.split('/').skip(1) {
3642 if segment.is_empty() {
3643 route_segments.push(String::new());
3644 continue;
3645 }
3646 if segment.starts_with(':') || segment.starts_with('*') {
3647 return Err(invalid(
3648 "segments beginning with `:` or `*` conflict with Axum route syntax",
3649 ));
3650 }
3651
3652 let has_open = segment.contains('{');
3653 let has_close = segment.contains('}');
3654 if has_open || has_close {
3655 let Some(open) = segment.find('{') else {
3656 return Err(invalid(
3657 "path parameter has a closing brace without an opening brace",
3658 ));
3659 };
3660 let Some(relative_close) = segment[open + 1..].find('}') else {
3661 return Err(invalid(
3662 "path parameter has an opening brace without a closing brace",
3663 ));
3664 };
3665 let close = open + 1 + relative_close;
3666 let name = &segment[open + 1..close];
3667 let prefix = &segment[..open];
3668 let suffix = &segment[close + 1..];
3669 if name.is_empty() || name.contains(['{', '}']) {
3670 return Err(invalid(
3671 "path parameter names must be non-empty and cannot contain braces",
3672 ));
3673 }
3674 if prefix.contains(['{', '}']) || suffix.contains(['{', '}']) {
3675 return Err(invalid(
3676 "embedded path segments may contain exactly one parameter",
3677 ));
3678 }
3679 route_segments.push(format!("{{{name}}}"));
3680 } else {
3681 route_segments.push(segment.to_string());
3682 }
3683 }
3684
3685 Ok(format!("/{}", route_segments.join("/")))
3686}
3687
3688fn body_type(op: &OperationInfo) -> Option<String> {
3689 match &op.request_body {
3690 Some(RequestBodyContent::Json { schema_name, .. })
3691 | Some(RequestBodyContent::FormUrlEncoded { schema_name, .. })
3692 | Some(RequestBodyContent::Multipart { schema_name, .. }) => Some(schema_name.clone()),
3693 Some(RequestBodyContent::OctetStream { .. } | RequestBodyContent::Binary { .. }) => {
3694 Some("bytes::Bytes".to_string())
3695 }
3696 Some(RequestBodyContent::TextPlain { .. }) => Some("String".to_string()),
3697 _ => None,
3698 }
3699}
3700
3701fn group_by_tag<'a>(ops: &[&'a OperationInfo]) -> BTreeMap<String, Vec<&'a OperationInfo>> {
3702 let mut groups: BTreeMap<String, Vec<&OperationInfo>> = BTreeMap::new();
3703 for op in ops {
3704 let tag = primary_tag(op);
3705 groups.entry(tag).or_default().push(op);
3706 }
3707 groups
3708}
3709
3710fn primary_tag(op: &OperationInfo) -> String {
3711 op.tags.first().cloned().unwrap_or_else(|| "Server".into())
3712}
3713
3714fn validate_tag_identifier_collisions(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
3718 let raw_tags: std::collections::BTreeSet<String> =
3719 ops.iter().map(|operation| primary_tag(operation)).collect();
3720 let mut raw_by_identifier: BTreeMap<String, String> = BTreeMap::new();
3721 for raw_tag in raw_tags {
3722 let identifier = trait_ident_for_tag(&raw_tag).to_string();
3723 if let Some(first_tag) = raw_by_identifier.insert(identifier.clone(), raw_tag.clone()) {
3724 return Err(ServerCodegenError::TagIdentifierCollision {
3725 first_tag,
3726 second_tag: raw_tag,
3727 identifier,
3728 });
3729 }
3730 }
3731 Ok(())
3732}
3733
3734fn validate_custom_method_route_groups(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
3735 let mut tags_by_path: BTreeMap<&str, std::collections::BTreeSet<String>> = BTreeMap::new();
3736 for op in ops {
3737 if axum_method_call(&op.method).is_none() {
3738 tags_by_path
3739 .entry(&op.path)
3740 .or_default()
3741 .insert(primary_tag(op));
3742 }
3743 }
3744 if let Some((path, tags)) = tags_by_path.into_iter().find(|(_, tags)| tags.len() > 1) {
3745 return Err(ServerCodegenError::CrossTagCustomMethods {
3746 path: path.to_string(),
3747 tags: tags.into_iter().collect::<Vec<_>>().join(", "),
3748 });
3749 }
3750 Ok(())
3751}
3752
3753fn canonical_axum_route_shape(path: &str) -> String {
3754 path.split('/')
3755 .map(|segment| {
3756 if segment.starts_with('{') && segment.ends_with('}') {
3757 "{}"
3758 } else {
3759 segment
3760 }
3761 })
3762 .collect::<Vec<_>>()
3763 .join("/")
3764}
3765
3766fn validate_normalized_route_collisions(ops: &[&OperationInfo]) -> Result<(), ServerCodegenError> {
3767 let mut seen: BTreeMap<(String, String), &str> = BTreeMap::new();
3768 for op in ops {
3769 let normalized = openapi_to_axum_path(&op.path)?;
3770 let key = (
3771 canonical_axum_route_shape(&normalized),
3772 op.method.to_ascii_uppercase(),
3773 );
3774 if let Some(previous) = seen.insert(key, &op.path)
3775 && previous != op.path
3776 {
3777 return Err(ServerCodegenError::InvalidRoutePath {
3778 path: op.path.clone(),
3779 reason: format!(
3780 "normalizes to the same Axum route and method as `{previous}`; embedded path affixes must remain unambiguous"
3781 ),
3782 });
3783 }
3784 }
3785 Ok(())
3786}
3787
3788fn trait_ident_for_tag(tag: &str) -> syn::Ident {
3789 let pascal = tag.to_pascal_case();
3790 let base = if pascal.is_empty() {
3791 "Server".into()
3792 } else {
3793 pascal
3794 };
3795 format_ident!("{}Api", base)
3796}
3797
3798pub fn status_variant_name(status: &str) -> String {
3802 match status {
3803 "200" => "Ok".into(),
3804 "201" => "Created".into(),
3805 "202" => "Accepted".into(),
3806 "204" => "NoContent".into(),
3807 "301" => "MovedPermanently".into(),
3808 "302" => "Found".into(),
3809 "304" => "NotModified".into(),
3810 "400" => "BadRequest".into(),
3811 "401" => "Unauthorized".into(),
3812 "403" => "Forbidden".into(),
3813 "404" => "NotFound".into(),
3814 "409" => "Conflict".into(),
3815 "410" => "Gone".into(),
3816 "422" => "UnprocessableEntity".into(),
3817 "429" => "TooManyRequests".into(),
3818 "500" => "InternalServerError".into(),
3819 "502" => "BadGateway".into(),
3820 "503" => "ServiceUnavailable".into(),
3821 "default" => "Default".into(),
3822 "1XX" => "Informational".into(),
3823 "2XX" => "Success".into(),
3824 "3XX" => "Redirection".into(),
3825 "4XX" => "ClientError".into(),
3826 "5XX" => "ServerError".into(),
3827 other => format!("Status{}", other.to_ascii_uppercase().replace('X', "x")),
3828 }
3829}
3830
3831fn response_uses_runtime_status(status: &str) -> bool {
3832 status == "default" || matches!(status.as_bytes(), [b'1'..=b'5', b'X' | b'x', b'X' | b'x'])
3833}
3834
3835fn runtime_status_guard(status: &str, value: TokenStream) -> TokenStream {
3836 if status == "default" {
3837 quote! { true }
3838 } else {
3839 let class = u16::from(
3840 status
3841 .as_bytes()
3842 .first()
3843 .map(|digit| digit - b'0')
3844 .unwrap_or_default(),
3845 );
3846 quote! { #value.as_u16() / 100 == #class }
3847 }
3848}
3849
3850fn status_token(status: &str) -> TokenStream {
3854 match status {
3855 "200" => quote! { StatusCode::OK },
3856 "201" => quote! { StatusCode::CREATED },
3857 "202" => quote! { StatusCode::ACCEPTED },
3858 "204" => quote! { StatusCode::NO_CONTENT },
3859 "301" => quote! { StatusCode::MOVED_PERMANENTLY },
3860 "302" => quote! { StatusCode::FOUND },
3861 "304" => quote! { StatusCode::NOT_MODIFIED },
3862 "400" => quote! { StatusCode::BAD_REQUEST },
3863 "401" => quote! { StatusCode::UNAUTHORIZED },
3864 "403" => quote! { StatusCode::FORBIDDEN },
3865 "404" => quote! { StatusCode::NOT_FOUND },
3866 "409" => quote! { StatusCode::CONFLICT },
3867 "410" => quote! { StatusCode::GONE },
3868 "422" => quote! { StatusCode::UNPROCESSABLE_ENTITY },
3869 "429" => quote! { StatusCode::TOO_MANY_REQUESTS },
3870 "500" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
3871 "502" => quote! { StatusCode::BAD_GATEWAY },
3872 "503" => quote! { StatusCode::SERVICE_UNAVAILABLE },
3873 "default" => quote! { StatusCode::INTERNAL_SERVER_ERROR },
3874 "1XX" | "2XX" | "3XX" | "4XX" | "5XX" => {
3877 quote! { StatusCode::INTERNAL_SERVER_ERROR }
3878 }
3879 other => {
3884 if let Ok(n) = other.parse::<u16>() {
3885 quote! {
3886 StatusCode::from_u16(#n).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
3887 }
3888 } else {
3889 quote! { StatusCode::INTERNAL_SERVER_ERROR }
3890 }
3891 }
3892 }
3893}
3894
3895fn parse_type(ty: &str) -> TokenStream {
3896 syn::parse_str::<syn::Type>(ty)
3897 .map(|t| quote! { #t })
3898 .unwrap_or_else(|_| {
3899 let ident = format_ident!("{}", ty);
3900 quote! { #ident }
3901 })
3902}
3903
3904fn format_or_raw(ts: TokenStream) -> String {
3905 let raw = ts.to_string();
3906 match syn::parse_file(&raw) {
3907 Ok(parsed) => prettyplease::unparse(&parsed),
3908 Err(_) => raw,
3909 }
3910}
3911
3912#[cfg(test)]
3913mod tests {
3914 use super::*;
3915
3916 #[test]
3917 fn status_variant_name_maps_known_codes() {
3918 assert_eq!(status_variant_name("200"), "Ok");
3919 assert_eq!(status_variant_name("4XX"), "ClientError");
3920 assert_eq!(status_variant_name("default"), "Default");
3921 assert_eq!(status_variant_name("418"), "Status418");
3922 }
3923
3924 #[test]
3925 fn trait_ident_for_tag_appends_api() {
3926 let id = trait_ident_for_tag("Responses");
3927 assert_eq!(id.to_string(), "ResponsesApi");
3928 }
3929
3930 #[test]
3931 fn embedded_path_affix_route_collisions_are_rejected() {
3932 let json = OperationInfo {
3933 operation_id: "json".into(),
3934 method: "get".into(),
3935 path: "/specs/{id}.json".into(),
3936 ..Default::default()
3937 };
3938 let yaml = OperationInfo {
3939 operation_id: "yaml".into(),
3940 method: "get".into(),
3941 path: "/specs/{name}.yaml".into(),
3942 ..Default::default()
3943 };
3944 let error = validate_normalized_route_collisions(&[&json, &yaml]).unwrap_err();
3945 assert!(error.to_string().contains("same Axum route"), "{error}");
3946 }
3947
3948 #[test]
3949 fn untagged_falls_back_to_server_api() {
3950 let id = trait_ident_for_tag("");
3951 assert_eq!(id.to_string(), "ServerApi");
3952 }
3953
3954 #[test]
3955 fn colliding_raw_tags_are_rejected_in_stable_order() {
3956 let first = OperationInfo {
3957 operation_id: "first".into(),
3958 tags: vec!["foo_bar".into()],
3959 ..Default::default()
3960 };
3961 let second = OperationInfo {
3962 operation_id: "second".into(),
3963 tags: vec!["foo-bar".into()],
3964 ..Default::default()
3965 };
3966 let error = validate_tag_identifier_collisions(&[&first, &second]).unwrap_err();
3967 assert!(matches!(
3968 error,
3969 ServerCodegenError::TagIdentifierCollision {
3970 first_tag,
3971 second_tag,
3972 identifier,
3973 } if first_tag == "foo-bar"
3974 && second_tag == "foo_bar"
3975 && identifier == "FooBarApi"
3976 ));
3977 }
3978
3979 #[test]
3980 fn custom_methods_on_one_path_share_an_exact_dispatcher() {
3981 let dispatcher = format_ident!("cache_custom_method_dispatch");
3982 let methods = vec![
3983 ("PURGE".to_string(), format_ident!("purge_cache_handler")),
3984 ("QUERY".to_string(), format_ident!("query_cache_handler")),
3985 ];
3986 let trait_ident = format_ident!("CacheApi");
3987 let (route, dispatcher) = axum_custom_route("/cache", &dispatcher, &methods, &trait_ident);
3988 let route = route.to_string();
3989 let dispatcher = dispatcher.to_string();
3990 assert!(route.contains("routing :: any"));
3991 assert_eq!(route.matches("routing :: any").count(), 1);
3992 assert!(dispatcher.contains("\"PURGE\""));
3993 assert!(dispatcher.contains("\"QUERY\""));
3994 assert!(dispatcher.contains("purge_cache_handler"));
3995 assert!(dispatcher.contains("query_cache_handler"));
3996 assert!(dispatcher.contains("METHOD_NOT_ALLOWED"));
3997 }
3998
3999 #[test]
4000 fn standard_methods_use_axum_method_routes_without_guards() {
4001 assert!(axum_method_call("TRACE").is_some());
4002 assert!(axum_method_call("QUERY").is_none());
4003 }
4004
4005 #[test]
4006 fn openapi_parameterized_paths_are_axum_08_paths() {
4007 assert_eq!(
4008 openapi_to_axum_path("/pets/{pet_id}").unwrap(),
4009 "/pets/{pet_id}"
4010 );
4011 assert_eq!(openapi_to_axum_path("/").unwrap(), "/");
4012 assert_eq!(
4013 openapi_to_axum_path("/specs/{provider}/{api}.json").unwrap(),
4014 "/specs/{provider}/{api}"
4015 );
4016 }
4017
4018 #[test]
4019 fn embedded_path_parameter_affixes_are_preserved_for_extraction() {
4020 assert_eq!(
4021 path_parameter_affixes("/specs/{provider}/{api}.json", "api"),
4022 Some(("", ".json"))
4023 );
4024 assert_eq!(
4025 path_parameter_affixes("/{provider}.json", "provider"),
4026 Some(("", ".json"))
4027 );
4028 assert_eq!(path_parameter_affixes("/pets/{id}", "id"), None);
4029 }
4030
4031 #[test]
4032 fn malformed_or_unsupported_route_templates_are_rejected() {
4033 for path in [
4034 "pets/{pet_id}",
4035 "/pets/{first}-{second}",
4036 "/pets/{}",
4037 "/pets/:id",
4038 ] {
4039 assert!(
4040 matches!(
4041 openapi_to_axum_path(path),
4042 Err(ServerCodegenError::InvalidRoutePath { .. })
4043 ),
4044 "{path} should be rejected"
4045 );
4046 }
4047 }
4048}