@@ -77,12 +77,28 @@ fn extract_status_code_tuple(err_ty: &Type) -> Option<(u16, Type)> {
7777 }
7878}
7979
80+ /// Check if a type is a non-body response type (metadata only).
81+ /// These types contribute to the HTTP response (status, headers, cookies)
82+ /// but do not form the response body.
83+ fn is_non_body_type ( ty : & Type ) -> bool {
84+ is_keyword_type ( ty, & KeywordType :: StatusCode )
85+ || is_keyword_type ( ty, & KeywordType :: HeaderMap )
86+ || is_keyword_type ( ty, & KeywordType :: CookieJar )
87+ }
88+
8089/// Extract payload type from an Ok tuple and track if headers exist.
81- /// The last element of the tuple is always treated as the response body.
90+ /// Non-body types (`StatusCode`, `HeaderMap`, `CookieJar`) are filtered out.
91+ /// The last remaining element is treated as the response body.
8292/// Any presence of `HeaderMap` in the tuple marks headers as present.
8393fn extract_ok_payload_and_headers ( ok_ty : & Type ) -> ( Type , Option < HashMap < String , Header > > ) {
8494 if let Type :: Tuple ( tuple) = ok_ty {
85- let payload_ty = tuple. elems . last ( ) . map ( |ty| unwrap_json ( ty) . clone ( ) ) ;
95+ // Find the body type: last element that is NOT a non-body type
96+ let payload_ty = tuple
97+ . elems
98+ . iter ( )
99+ . rev ( )
100+ . find ( |ty| !is_non_body_type ( ty) )
101+ . map ( |ty| unwrap_json ( ty) . clone ( ) ) ;
86102
87103 if let Some ( payload_ty) = payload_ty {
88104 let headers = if tuple
@@ -127,27 +143,34 @@ pub fn parse_return_type(
127143 if let Some ( ( ok_ty, err_ty) ) = extract_result_types ( ty) {
128144 // Handle success response (200)
129145 let ( ok_payload_ty, ok_headers) = extract_ok_payload_and_headers ( & ok_ty) ;
130- let ok_schema = parse_type_to_schema_ref_with_schemas (
131- & ok_payload_ty,
132- known_schemas,
133- struct_definitions,
134- ) ;
135- let mut ok_content = BTreeMap :: new ( ) ;
136- ok_content. insert (
137- "application/json" . to_string ( ) ,
138- MediaType {
139- schema : Some ( ok_schema) ,
140- example : None ,
141- examples : None ,
142- } ,
143- ) ;
146+
147+ // StatusCode alone means no response body — just the HTTP status code
148+ let ok_content = if is_keyword_type ( & ok_payload_ty, & KeywordType :: StatusCode ) {
149+ None
150+ } else {
151+ let ok_schema = parse_type_to_schema_ref_with_schemas (
152+ & ok_payload_ty,
153+ known_schemas,
154+ struct_definitions,
155+ ) ;
156+ let mut content = BTreeMap :: new ( ) ;
157+ content. insert (
158+ "application/json" . to_string ( ) ,
159+ MediaType {
160+ schema : Some ( ok_schema) ,
161+ example : None ,
162+ examples : None ,
163+ } ,
164+ ) ;
165+ Some ( content)
166+ } ;
144167
145168 responses. insert (
146169 "200" . to_string ( ) ,
147170 Response {
148171 description : "Successful response" . to_string ( ) ,
149172 headers : ok_headers,
150- content : Some ( ok_content) ,
173+ content : ok_content,
151174 } ,
152175 ) ;
153176
@@ -210,27 +233,34 @@ pub fn parse_return_type(
210233 // Not a Result type - regular response
211234 // Unwrap Json<T> if present
212235 let unwrapped_ty = unwrap_json ( ty) ;
213- let schema = parse_type_to_schema_ref_with_schemas (
214- unwrapped_ty,
215- known_schemas,
216- struct_definitions,
217- ) ;
218- let mut content = BTreeMap :: new ( ) ;
219- content. insert (
220- "application/json" . to_string ( ) ,
221- MediaType {
222- schema : Some ( schema) ,
223- example : None ,
224- examples : None ,
225- } ,
226- ) ;
236+
237+ // StatusCode alone means no response body
238+ let content = if is_keyword_type ( unwrapped_ty, & KeywordType :: StatusCode ) {
239+ None
240+ } else {
241+ let schema = parse_type_to_schema_ref_with_schemas (
242+ unwrapped_ty,
243+ known_schemas,
244+ struct_definitions,
245+ ) ;
246+ let mut c = BTreeMap :: new ( ) ;
247+ c. insert (
248+ "application/json" . to_string ( ) ,
249+ MediaType {
250+ schema : Some ( schema) ,
251+ example : None ,
252+ examples : None ,
253+ } ,
254+ ) ;
255+ Some ( c)
256+ } ;
227257
228258 responses. insert (
229259 "200" . to_string ( ) ,
230260 Response {
231261 description : "Successful response" . to_string ( ) ,
232262 headers : None ,
233- content : Some ( content ) ,
263+ content,
234264 } ,
235265 ) ;
236266 }
@@ -381,6 +411,27 @@ mod tests {
381411 Some ( ExpectedResponse { status: "400" , schema: ExpectedSchema { schema_type: SchemaType :: Integer , nullable: false , items_schema_type: None } } ) ,
382412 None
383413 ) ]
414+ // StatusCode as the sole Ok response type → no content (empty body)
415+ #[ case(
416+ "-> Result<StatusCode, (StatusCode, String)>" ,
417+ None ,
418+ Some ( ExpectedResponse { status: "400" , schema: ExpectedSchema { schema_type: SchemaType :: String , nullable: false , items_schema_type: None } } ) ,
419+ None
420+ ) ]
421+ // CookieJar in Ok tuple → body is Json<String>, CookieJar filtered out
422+ #[ case(
423+ "-> Result<(CookieJar, Json<String>), (StatusCode, String)>" ,
424+ Some ( ExpectedSchema { schema_type: SchemaType :: String , nullable: false , items_schema_type: None } ) ,
425+ Some ( ExpectedResponse { status: "400" , schema: ExpectedSchema { schema_type: SchemaType :: String , nullable: false , items_schema_type: None } } ) ,
426+ None
427+ ) ]
428+ // CookieJar + StatusCode in Ok tuple → body is last non-metadata element
429+ #[ case(
430+ "-> Result<(StatusCode, CookieJar, Json<i32>), String>" ,
431+ Some ( ExpectedSchema { schema_type: SchemaType :: Integer , nullable: false , items_schema_type: None } ) ,
432+ Some ( ExpectedResponse { status: "400" , schema: ExpectedSchema { schema_type: SchemaType :: String , nullable: false , items_schema_type: None } } ) ,
433+ None
434+ ) ]
384435 fn test_parse_return_type (
385436 #[ case] return_type_str : & str ,
386437 #[ case] ok_expectation : Option < ExpectedSchema > ,
@@ -610,4 +661,58 @@ mod tests {
610661 // Headers should be None
611662 assert ! ( ok_response. headers. is_none( ) ) ;
612663 }
664+
665+ // ======== CookieJar tuple extraction tests ========
666+
667+ #[ test]
668+ fn test_extract_ok_payload_and_headers_cookie_jar_tuple ( ) {
669+ // (CookieJar, Json<String>) → payload should be String, CookieJar filtered
670+ let ty: syn:: Type = syn:: parse_str ( "(CookieJar, Json<String>)" ) . unwrap ( ) ;
671+ let ( payload, headers) = extract_ok_payload_and_headers ( & ty) ;
672+
673+ if let syn:: Type :: Path ( type_path) = & payload {
674+ assert_eq ! (
675+ type_path. path. segments. last( ) . unwrap( ) . ident. to_string( ) ,
676+ "String"
677+ ) ;
678+ } else {
679+ panic ! ( "Expected Path type for payload" ) ;
680+ }
681+ assert ! ( headers. is_none( ) ) ;
682+ }
683+
684+ #[ test]
685+ fn test_extract_ok_payload_and_headers_cookie_jar_with_status_code ( ) {
686+ // (StatusCode, CookieJar, Json<i32>) → payload should be i32
687+ let ty: syn:: Type = syn:: parse_str ( "(StatusCode, CookieJar, Json<i32>)" ) . unwrap ( ) ;
688+ let ( payload, headers) = extract_ok_payload_and_headers ( & ty) ;
689+
690+ if let syn:: Type :: Path ( type_path) = & payload {
691+ assert_eq ! (
692+ type_path. path. segments. last( ) . unwrap( ) . ident. to_string( ) ,
693+ "i32"
694+ ) ;
695+ } else {
696+ panic ! ( "Expected Path type for payload" ) ;
697+ }
698+ assert ! ( headers. is_none( ) ) ;
699+ }
700+
701+ #[ test]
702+ fn test_is_non_body_type ( ) {
703+ let status: syn:: Type = syn:: parse_str ( "StatusCode" ) . unwrap ( ) ;
704+ assert ! ( is_non_body_type( & status) ) ;
705+
706+ let header_map: syn:: Type = syn:: parse_str ( "HeaderMap" ) . unwrap ( ) ;
707+ assert ! ( is_non_body_type( & header_map) ) ;
708+
709+ let cookie_jar: syn:: Type = syn:: parse_str ( "CookieJar" ) . unwrap ( ) ;
710+ assert ! ( is_non_body_type( & cookie_jar) ) ;
711+
712+ let string: syn:: Type = syn:: parse_str ( "String" ) . unwrap ( ) ;
713+ assert ! ( !is_non_body_type( & string) ) ;
714+
715+ let json: syn:: Type = syn:: parse_str ( "Json<String>" ) . unwrap ( ) ;
716+ assert ! ( !is_non_body_type( & json) ) ;
717+ }
613718}
0 commit comments