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