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