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