33//! # Body Resolution
44//!
55//! Logic for extracting request body types from OpenAPI definitions.
6+ //! Support for OAS 3.2 `encoding` definitions in multipart and url-encoded forms.
67
78use crate :: error:: AppResult ;
89use crate :: oas:: models:: { BodyFormat , RequestBodyDefinition } ;
910use crate :: oas:: resolver:: types:: map_schema_to_rust_type;
11+ use std:: collections:: HashMap ;
12+ use utoipa:: openapi:: encoding:: Encoding ;
1013use utoipa:: openapi:: request_body:: RequestBody ;
1114use utoipa:: openapi:: RefOr ;
1215
@@ -19,38 +22,151 @@ pub fn extract_request_body_type(
1922 RefOr :: Ref ( _) => return Ok ( None ) ,
2023 } ;
2124
25+ // 1. JSON
2226 if let Some ( media) = content. get ( "application/json" ) {
2327 if let Some ( schema_ref) = & media. schema {
2428 let type_str = map_schema_to_rust_type ( schema_ref, true ) ?;
2529 return Ok ( Some ( RequestBodyDefinition {
2630 ty : type_str,
2731 format : BodyFormat :: Json ,
32+ encoding : None ,
2833 } ) ) ;
2934 }
3035 }
3136
37+ // 2. Form URL Encoded
3238 if let Some ( media) = content. get ( "application/x-www-form-urlencoded" ) {
3339 if let Some ( schema_ref) = & media. schema {
3440 let type_str = map_schema_to_rust_type ( schema_ref, true ) ?;
41+ let encoding = extract_encoding_map ( & media. encoding ) ;
42+
3543 return Ok ( Some ( RequestBodyDefinition {
3644 ty : type_str,
3745 format : BodyFormat :: Form ,
46+ encoding,
3847 } ) ) ;
3948 }
4049 }
4150
42- if let Some ( media) = content. get ( "multipart/form-data" ) {
51+ // 3. Multipart
52+ // OAS 3.2 allows multipart/form-data, multipart/mixed, etc.
53+ // We check for any key starting with "multipart/"
54+ if let Some ( ( _, media) ) = content. iter ( ) . find ( |( k, _) | k. starts_with ( "multipart/" ) ) {
4355 let type_str = if let Some ( schema_ref) = & media. schema {
4456 map_schema_to_rust_type ( schema_ref, true ) ?
4557 } else {
4658 "Multipart" . to_string ( )
4759 } ;
4860
61+ let encoding = extract_encoding_map ( & media. encoding ) ;
62+
4963 return Ok ( Some ( RequestBodyDefinition {
5064 ty : type_str,
5165 format : BodyFormat :: Multipart ,
66+ encoding,
5267 } ) ) ;
5368 }
5469
5570 Ok ( None )
5671}
72+
73+ /// Helper to extract encoding map (property -> content-type).
74+ fn extract_encoding_map (
75+ encoding : & std:: collections:: BTreeMap < String , Encoding > ,
76+ ) -> Option < HashMap < String , String > > {
77+ if encoding. is_empty ( ) {
78+ return None ;
79+ }
80+
81+ let mut map = HashMap :: new ( ) ;
82+ for ( prop, enc) in encoding {
83+ // According to OAS 3.2: contentType is string.
84+ // We capture it to allow specific part handling in Strategies.
85+ if let Some ( ct) = & enc. content_type {
86+ map. insert ( prop. clone ( ) , ct. clone ( ) ) ;
87+ }
88+ }
89+
90+ if map. is_empty ( ) {
91+ None
92+ } else {
93+ Some ( map)
94+ }
95+ }
96+
97+ #[ cfg( test) ]
98+ mod tests {
99+ use super :: * ;
100+ use utoipa:: openapi:: encoding:: Encoding ;
101+ use utoipa:: openapi:: request_body:: RequestBodyBuilder ;
102+ use utoipa:: openapi:: Content ;
103+
104+ #[ test]
105+ fn test_extract_json_body ( ) {
106+ let body = RequestBodyBuilder :: new ( )
107+ . content (
108+ "application/json" ,
109+ Content :: new ( Some ( RefOr :: Ref ( utoipa:: openapi:: Ref :: new (
110+ "#/components/schemas/User" ,
111+ ) ) ) ) ,
112+ )
113+ . build ( ) ;
114+
115+ let def = extract_request_body_type ( & RefOr :: T ( body) ) . unwrap ( ) . unwrap ( ) ;
116+ assert_eq ! ( def. ty, "User" ) ;
117+ assert_eq ! ( def. format, BodyFormat :: Json ) ;
118+ assert ! ( def. encoding. is_none( ) ) ;
119+ }
120+
121+ #[ test]
122+ fn test_extract_multipart_with_encoding ( ) {
123+ // Encoding::builder().content_type(...) takes Option<String>
124+ let png_encoding = Encoding :: builder ( )
125+ . content_type ( Some ( "image/png" . to_string ( ) ) )
126+ . build ( ) ;
127+ let json_encoding = Encoding :: builder ( )
128+ . content_type ( Some ( "application/json" . to_string ( ) ) )
129+ . build ( ) ;
130+
131+ // ContentBuilder::encoding takes (name, encoding) one by one
132+ let media = Content :: builder ( )
133+ . schema ( Some ( RefOr :: Ref ( utoipa:: openapi:: Ref :: new (
134+ "#/components/schemas/Upload" ,
135+ ) ) ) )
136+ . encoding ( "profileImage" , png_encoding)
137+ . encoding ( "metadata" , json_encoding)
138+ . build ( ) ;
139+
140+ let body = RequestBodyBuilder :: new ( )
141+ . content ( "multipart/form-data" , media)
142+ . build ( ) ;
143+
144+ let def = extract_request_body_type ( & RefOr :: T ( body) ) . unwrap ( ) . unwrap ( ) ;
145+
146+ assert_eq ! ( def. ty, "Upload" ) ;
147+ assert_eq ! ( def. format, BodyFormat :: Multipart ) ;
148+
149+ let enc = def. encoding . unwrap ( ) ;
150+ assert_eq ! ( enc. get( "profileImage" ) , Some ( & "image/png" . to_string( ) ) ) ;
151+ assert_eq ! ( enc. get( "metadata" ) , Some ( & "application/json" . to_string( ) ) ) ;
152+ }
153+
154+ #[ test]
155+ fn test_extract_form_no_encoding ( ) {
156+ let request_body = RequestBodyBuilder :: new ( )
157+ . content (
158+ "application/x-www-form-urlencoded" ,
159+ Content :: new ( Some ( RefOr :: Ref ( utoipa:: openapi:: Ref :: new (
160+ "#/components/schemas/Login" ,
161+ ) ) ) ) ,
162+ )
163+ . build ( ) ;
164+
165+ let def = extract_request_body_type ( & RefOr :: T ( request_body) )
166+ . unwrap ( )
167+ . unwrap ( ) ;
168+ assert_eq ! ( def. ty, "Login" ) ;
169+ assert_eq ! ( def. format, BodyFormat :: Form ) ;
170+ assert ! ( def. encoding. is_none( ) ) ;
171+ }
172+ }
0 commit comments