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