@@ -25,13 +25,15 @@ pub fn extract_path_parameters(path: &str) -> Vec<String> {
2525 params
2626}
2727
28- /// Analyze function parameter and convert to OpenAPI Parameter
28+ /// Analyze function parameter and convert to OpenAPI Parameter(s)
29+ /// Returns None if parameter should be ignored (e.g., Query<HashMap<...>>)
30+ /// Returns Some(Vec<Parameter>) with one or more parameters
2931pub fn parse_function_parameter (
3032 arg : & FnArg ,
3133 path_params : & [ String ] ,
3234 known_schemas : & HashMap < String , String > ,
3335 struct_definitions : & HashMap < String , String > ,
34- ) -> Option < Parameter > {
36+ ) -> Option < Vec < Parameter > > {
3537 match arg {
3638 FnArg :: Receiver ( _) => None ,
3739 FnArg :: Typed ( PatType { pat, ty, .. } ) => {
@@ -75,7 +77,7 @@ pub fn parse_function_parameter(
7577 // Otherwise use the parameter name from the pattern
7678 param_name
7779 } ;
78- return Some ( Parameter {
80+ return Some ( vec ! [ Parameter {
7981 name,
8082 r#in: ParameterLocation :: Path ,
8183 description: None ,
@@ -86,7 +88,7 @@ pub fn parse_function_parameter(
8688 struct_definitions,
8789 ) ) ,
8890 example: None ,
89- } ) ;
91+ } ] ) ;
9092 }
9193 }
9294 "Query" => {
@@ -95,7 +97,28 @@ pub fn parse_function_parameter(
9597 && let Some ( syn:: GenericArgument :: Type ( inner_ty) ) =
9698 args. args . first ( )
9799 {
98- return Some ( Parameter {
100+ // Check if it's HashMap or BTreeMap - ignore these
101+ if is_map_type ( inner_ty) {
102+ return None ;
103+ }
104+
105+ // Check if it's a struct - expand to individual parameters
106+ if let Some ( struct_params) = parse_query_struct_to_parameters (
107+ inner_ty,
108+ known_schemas,
109+ struct_definitions,
110+ ) {
111+ return Some ( struct_params) ;
112+ }
113+
114+ // Check if it's a known type (primitive or known schema)
115+ // If unknown, don't add parameter
116+ if !is_known_type ( inner_ty, known_schemas, struct_definitions) {
117+ return None ;
118+ }
119+
120+ // Otherwise, treat as single parameter
121+ return Some ( vec ! [ Parameter {
99122 name: param_name. clone( ) ,
100123 r#in: ParameterLocation :: Query ,
101124 description: None ,
@@ -106,7 +129,7 @@ pub fn parse_function_parameter(
106129 struct_definitions,
107130 ) ) ,
108131 example: None ,
109- } ) ;
132+ } ] ) ;
110133 }
111134 }
112135 "Header" => {
@@ -115,7 +138,7 @@ pub fn parse_function_parameter(
115138 && let Some ( syn:: GenericArgument :: Type ( inner_ty) ) =
116139 args. args . first ( )
117140 {
118- return Some ( Parameter {
141+ return Some ( vec ! [ Parameter {
119142 name: param_name. clone( ) ,
120143 r#in: ParameterLocation :: Header ,
121144 description: None ,
@@ -126,7 +149,7 @@ pub fn parse_function_parameter(
126149 struct_definitions,
127150 ) ) ,
128151 example: None ,
129- } ) ;
152+ } ] ) ;
130153 }
131154 }
132155 "Json" => {
@@ -140,7 +163,7 @@ pub fn parse_function_parameter(
140163
141164 // Check if it's a path parameter (by name match) - for non-extractor cases
142165 if path_params. contains ( & param_name) {
143- return Some ( Parameter {
166+ return Some ( vec ! [ Parameter {
144167 name: param_name. clone( ) ,
145168 r#in: ParameterLocation :: Path ,
146169 description: None ,
@@ -151,12 +174,12 @@ pub fn parse_function_parameter(
151174 struct_definitions,
152175 ) ) ,
153176 example: None ,
154- } ) ;
177+ } ] ) ;
155178 }
156179
157180 // Check if it's a primitive type (direct parameter)
158181 if is_primitive_type ( ty. as_ref ( ) ) {
159- return Some ( Parameter {
182+ return Some ( vec ! [ Parameter {
160183 name: param_name. clone( ) ,
161184 r#in: ParameterLocation :: Query ,
162185 description: None ,
@@ -167,14 +190,214 @@ pub fn parse_function_parameter(
167190 struct_definitions,
168191 ) ) ,
169192 example: None ,
170- } ) ;
193+ } ] ) ;
171194 }
172195
173196 None
174197 }
175198 }
176199}
177200
201+ /// Check if a type is HashMap or BTreeMap
202+ fn is_map_type ( ty : & Type ) -> bool {
203+ if let Type :: Path ( type_path) = ty {
204+ let path = & type_path. path ;
205+ if !path. segments . is_empty ( ) {
206+ let segment = path. segments . last ( ) . unwrap ( ) ;
207+ let ident_str = segment. ident . to_string ( ) ;
208+ return ident_str == "HashMap" || ident_str == "BTreeMap" ;
209+ }
210+ }
211+ false
212+ }
213+
214+ /// Check if a type is a known type (primitive, known schema, or struct definition)
215+ fn is_known_type (
216+ ty : & Type ,
217+ known_schemas : & HashMap < String , String > ,
218+ struct_definitions : & HashMap < String , String > ,
219+ ) -> bool {
220+ // Check if it's a primitive type
221+ if is_primitive_type ( ty) {
222+ return true ;
223+ }
224+
225+ // Check if it's a known struct
226+ if let Type :: Path ( type_path) = ty {
227+ let path = & type_path. path ;
228+ if path. segments . is_empty ( ) {
229+ return false ;
230+ }
231+
232+ let segment = path. segments . last ( ) . unwrap ( ) ;
233+ let ident_str = segment. ident . to_string ( ) ;
234+
235+ // Get type name (handle both simple and qualified paths)
236+ let type_name = if path. segments . len ( ) > 1 {
237+ ident_str. clone ( )
238+ } else {
239+ ident_str. clone ( )
240+ } ;
241+
242+ // Check if it's in struct_definitions or known_schemas
243+ if struct_definitions. contains_key ( & type_name) || known_schemas. contains_key ( & type_name) {
244+ return true ;
245+ }
246+
247+ // Check for generic types like Vec<T>, Option<T> - recursively check inner type
248+ if let syn:: PathArguments :: AngleBracketed ( args) = & segment. arguments {
249+ match ident_str. as_str ( ) {
250+ "Vec" | "Option" => {
251+ if let Some ( syn:: GenericArgument :: Type ( inner_ty) ) = args. args . first ( ) {
252+ return is_known_type ( inner_ty, known_schemas, struct_definitions) ;
253+ }
254+ }
255+ _ => { }
256+ }
257+ }
258+ }
259+
260+ false
261+ }
262+
263+ /// Parse struct fields to individual query parameters
264+ /// Returns None if the type is not a struct or cannot be parsed
265+ fn parse_query_struct_to_parameters (
266+ ty : & Type ,
267+ known_schemas : & HashMap < String , String > ,
268+ struct_definitions : & HashMap < String , String > ,
269+ ) -> Option < Vec < Parameter > > {
270+ // Check if it's a known struct
271+ if let Type :: Path ( type_path) = ty {
272+ let path = & type_path. path ;
273+ if path. segments . is_empty ( ) {
274+ return None ;
275+ }
276+
277+ let segment = path. segments . last ( ) . unwrap ( ) ;
278+ let ident_str = segment. ident . to_string ( ) ;
279+
280+ // Get type name (handle both simple and qualified paths)
281+ let type_name = if path. segments . len ( ) > 1 {
282+ ident_str. clone ( )
283+ } else {
284+ ident_str. clone ( )
285+ } ;
286+
287+ // Check if it's a known struct
288+ if let Some ( struct_def) = struct_definitions. get ( & type_name) {
289+ if let Ok ( struct_item) = syn:: parse_str :: < syn:: ItemStruct > ( struct_def) {
290+ let mut parameters = Vec :: new ( ) ;
291+
292+ // Extract rename_all attribute from struct
293+ let rename_all = extract_rename_all ( & struct_item. attrs ) ;
294+
295+ if let syn:: Fields :: Named ( fields_named) = & struct_item. fields {
296+ for field in & fields_named. named {
297+ let rust_field_name = field
298+ . ident
299+ . as_ref ( )
300+ . map ( |i| i. to_string ( ) )
301+ . unwrap_or_else ( || "unknown" . to_string ( ) ) ;
302+
303+ // Check for field-level rename attribute first (takes precedence)
304+ let field_name = if let Some ( renamed) = extract_field_rename ( & field. attrs ) {
305+ renamed
306+ } else {
307+ // Apply rename_all transformation if present
308+ rename_field ( & rust_field_name, rename_all. as_deref ( ) )
309+ } ;
310+
311+ let field_type = & field. ty ;
312+
313+ // Check if field is Option<T>
314+ let is_optional = matches ! (
315+ field_type,
316+ Type :: Path ( type_path)
317+ if type_path
318+ . path
319+ . segments
320+ . first( )
321+ . map( |s| s. ident == "Option" )
322+ . unwrap_or( false )
323+ ) ;
324+
325+ // Parse field type to schema (inline, not ref)
326+ // For Query parameters, we need inline schemas, not refs
327+ let mut field_schema = parse_type_to_schema_ref_with_schemas (
328+ field_type,
329+ known_schemas,
330+ struct_definitions,
331+ ) ;
332+
333+ // Convert ref to inline if needed (Query parameters should not use refs)
334+ // If it's a ref to a known struct, get the struct definition and inline it
335+ if let SchemaRef :: Ref ( ref_ref) = & field_schema {
336+ // Try to extract type name from ref path (e.g., "#/components/schemas/User" -> "User")
337+ if let Some ( type_name) =
338+ ref_ref. ref_path . strip_prefix ( "#/components/schemas/" )
339+ {
340+ if let Some ( struct_def) = struct_definitions. get ( type_name) {
341+ if let Ok ( nested_struct_item) =
342+ syn:: parse_str :: < syn:: ItemStruct > ( struct_def)
343+ {
344+ // Parse the nested struct to schema (inline)
345+ let nested_schema = parse_struct_to_schema (
346+ & nested_struct_item,
347+ known_schemas,
348+ struct_definitions,
349+ ) ;
350+ field_schema = SchemaRef :: Inline ( Box :: new ( nested_schema) ) ;
351+ }
352+ }
353+ }
354+ }
355+
356+ // If it's Option<T>, make it nullable
357+ let final_schema = if is_optional {
358+ if let SchemaRef :: Inline ( mut schema) = field_schema {
359+ schema. nullable = Some ( true ) ;
360+ SchemaRef :: Inline ( schema)
361+ } else {
362+ // If still a ref, convert to inline object with nullable
363+ SchemaRef :: Inline ( Box :: new ( Schema {
364+ schema_type : Some ( SchemaType :: Object ) ,
365+ nullable : Some ( true ) ,
366+ ..Schema :: object ( )
367+ } ) )
368+ }
369+ } else {
370+ // If it's still a ref, convert to inline object
371+ match field_schema {
372+ SchemaRef :: Ref ( _) => {
373+ SchemaRef :: Inline ( Box :: new ( Schema :: new ( SchemaType :: Object ) ) )
374+ }
375+ SchemaRef :: Inline ( schema) => SchemaRef :: Inline ( schema) ,
376+ }
377+ } ;
378+
379+ let required = !is_optional;
380+
381+ parameters. push ( Parameter {
382+ name : field_name,
383+ r#in : ParameterLocation :: Query ,
384+ description : None ,
385+ required : Some ( required) ,
386+ schema : Some ( final_schema) ,
387+ example : None ,
388+ } ) ;
389+ }
390+ }
391+
392+ if !parameters. is_empty ( ) {
393+ return Some ( parameters) ;
394+ }
395+ }
396+ }
397+ }
398+ None
399+ }
400+
178401/// Check if a type is a primitive type
179402fn is_primitive_type ( ty : & Type ) -> bool {
180403 match ty {
@@ -1176,10 +1399,10 @@ pub fn build_operation_from_function(
11761399 // Check if it's a request body (Json<T>)
11771400 if let Some ( body) = parse_request_body ( input, known_schemas, struct_definitions) {
11781401 request_body = Some ( body) ;
1179- } else if let Some ( param ) =
1402+ } else if let Some ( params ) =
11801403 parse_function_parameter ( input, & path_params, known_schemas, struct_definitions)
11811404 {
1182- parameters. push ( param ) ;
1405+ parameters. extend ( params ) ;
11831406 }
11841407 }
11851408
0 commit comments