@@ -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,22 @@ 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+ // Otherwise, treat as single parameter
115+ return Some ( vec ! [ Parameter {
99116 name: param_name. clone( ) ,
100117 r#in: ParameterLocation :: Query ,
101118 description: None ,
@@ -106,7 +123,7 @@ pub fn parse_function_parameter(
106123 struct_definitions,
107124 ) ) ,
108125 example: None ,
109- } ) ;
126+ } ] ) ;
110127 }
111128 }
112129 "Header" => {
@@ -115,7 +132,7 @@ pub fn parse_function_parameter(
115132 && let Some ( syn:: GenericArgument :: Type ( inner_ty) ) =
116133 args. args . first ( )
117134 {
118- return Some ( Parameter {
135+ return Some ( vec ! [ Parameter {
119136 name: param_name. clone( ) ,
120137 r#in: ParameterLocation :: Header ,
121138 description: None ,
@@ -126,7 +143,7 @@ pub fn parse_function_parameter(
126143 struct_definitions,
127144 ) ) ,
128145 example: None ,
129- } ) ;
146+ } ] ) ;
130147 }
131148 }
132149 "Json" => {
@@ -140,7 +157,7 @@ pub fn parse_function_parameter(
140157
141158 // Check if it's a path parameter (by name match) - for non-extractor cases
142159 if path_params. contains ( & param_name) {
143- return Some ( Parameter {
160+ return Some ( vec ! [ Parameter {
144161 name: param_name. clone( ) ,
145162 r#in: ParameterLocation :: Path ,
146163 description: None ,
@@ -151,12 +168,12 @@ pub fn parse_function_parameter(
151168 struct_definitions,
152169 ) ) ,
153170 example: None ,
154- } ) ;
171+ } ] ) ;
155172 }
156173
157174 // Check if it's a primitive type (direct parameter)
158175 if is_primitive_type ( ty. as_ref ( ) ) {
159- return Some ( Parameter {
176+ return Some ( vec ! [ Parameter {
160177 name: param_name. clone( ) ,
161178 r#in: ParameterLocation :: Query ,
162179 description: None ,
@@ -167,14 +184,161 @@ pub fn parse_function_parameter(
167184 struct_definitions,
168185 ) ) ,
169186 example: None ,
170- } ) ;
187+ } ] ) ;
171188 }
172189
173190 None
174191 }
175192 }
176193}
177194
195+ /// Check if a type is HashMap or BTreeMap
196+ fn is_map_type ( ty : & Type ) -> bool {
197+ if let Type :: Path ( type_path) = ty {
198+ let path = & type_path. path ;
199+ if !path. segments . is_empty ( ) {
200+ let segment = path. segments . last ( ) . unwrap ( ) ;
201+ let ident_str = segment. ident . to_string ( ) ;
202+ return ident_str == "HashMap" || ident_str == "BTreeMap" ;
203+ }
204+ }
205+ false
206+ }
207+
208+ /// Parse struct fields to individual query parameters
209+ /// Returns None if the type is not a struct or cannot be parsed
210+ fn parse_query_struct_to_parameters (
211+ ty : & Type ,
212+ known_schemas : & HashMap < String , String > ,
213+ struct_definitions : & HashMap < String , String > ,
214+ ) -> Option < Vec < Parameter > > {
215+ // Check if it's a known struct
216+ if let Type :: Path ( type_path) = ty {
217+ let path = & type_path. path ;
218+ if path. segments . is_empty ( ) {
219+ return None ;
220+ }
221+
222+ let segment = path. segments . last ( ) . unwrap ( ) ;
223+ let ident_str = segment. ident . to_string ( ) ;
224+
225+ // Get type name (handle both simple and qualified paths)
226+ let type_name = if path. segments . len ( ) > 1 {
227+ ident_str. clone ( )
228+ } else {
229+ ident_str. clone ( )
230+ } ;
231+
232+ // Check if it's a known struct
233+ if let Some ( struct_def) = struct_definitions. get ( & type_name) {
234+ if let Ok ( struct_item) = syn:: parse_str :: < syn:: ItemStruct > ( struct_def) {
235+ let mut parameters = Vec :: new ( ) ;
236+
237+ // Extract rename_all attribute from struct
238+ let rename_all = extract_rename_all ( & struct_item. attrs ) ;
239+
240+ if let syn:: Fields :: Named ( fields_named) = & struct_item. fields {
241+ for field in & fields_named. named {
242+ let rust_field_name = field
243+ . ident
244+ . as_ref ( )
245+ . map ( |i| i. to_string ( ) )
246+ . unwrap_or_else ( || "unknown" . to_string ( ) ) ;
247+
248+ // Check for field-level rename attribute first (takes precedence)
249+ let field_name = if let Some ( renamed) = extract_field_rename ( & field. attrs ) {
250+ renamed
251+ } else {
252+ // Apply rename_all transformation if present
253+ rename_field ( & rust_field_name, rename_all. as_deref ( ) )
254+ } ;
255+
256+ let field_type = & field. ty ;
257+
258+ // Check if field is Option<T>
259+ let is_optional = matches ! (
260+ field_type,
261+ Type :: Path ( type_path)
262+ if type_path
263+ . path
264+ . segments
265+ . first( )
266+ . map( |s| s. ident == "Option" )
267+ . unwrap_or( false )
268+ ) ;
269+
270+ // Parse field type to schema (inline, not ref)
271+ // For Query parameters, we need inline schemas, not refs
272+ let mut field_schema = parse_type_to_schema_ref_with_schemas (
273+ field_type,
274+ known_schemas,
275+ struct_definitions,
276+ ) ;
277+
278+ // Convert ref to inline if needed (Query parameters should not use refs)
279+ // If it's a ref to a known struct, get the struct definition and inline it
280+ if let SchemaRef :: Ref ( ref_ref) = & field_schema {
281+ // Try to extract type name from ref path (e.g., "#/components/schemas/User" -> "User")
282+ if let Some ( type_name) = ref_ref. ref_path . strip_prefix ( "#/components/schemas/" ) {
283+ if let Some ( struct_def) = struct_definitions. get ( type_name) {
284+ if let Ok ( nested_struct_item) = syn:: parse_str :: < syn:: ItemStruct > ( struct_def) {
285+ // Parse the nested struct to schema (inline)
286+ let nested_schema = parse_struct_to_schema (
287+ & nested_struct_item,
288+ known_schemas,
289+ struct_definitions,
290+ ) ;
291+ field_schema = SchemaRef :: Inline ( Box :: new ( nested_schema) ) ;
292+ }
293+ }
294+ }
295+ }
296+
297+ // If it's Option<T>, make it nullable
298+ let final_schema = if is_optional {
299+ if let SchemaRef :: Inline ( mut schema) = field_schema {
300+ schema. nullable = Some ( true ) ;
301+ SchemaRef :: Inline ( schema)
302+ } else {
303+ // If still a ref, convert to inline object with nullable
304+ SchemaRef :: Inline ( Box :: new ( Schema {
305+ schema_type : Some ( SchemaType :: Object ) ,
306+ nullable : Some ( true ) ,
307+ ..Schema :: object ( )
308+ } ) )
309+ }
310+ } else {
311+ // If it's still a ref, convert to inline object
312+ match field_schema {
313+ SchemaRef :: Ref ( _) => {
314+ SchemaRef :: Inline ( Box :: new ( Schema :: new ( SchemaType :: Object ) ) )
315+ }
316+ SchemaRef :: Inline ( schema) => SchemaRef :: Inline ( schema) ,
317+ }
318+ } ;
319+
320+ let required = !is_optional;
321+
322+ parameters. push ( Parameter {
323+ name : field_name,
324+ r#in : ParameterLocation :: Query ,
325+ description : None ,
326+ required : Some ( required) ,
327+ schema : Some ( final_schema) ,
328+ example : None ,
329+ } ) ;
330+ }
331+ }
332+
333+ if !parameters. is_empty ( ) {
334+ return Some ( parameters) ;
335+ }
336+ }
337+ }
338+ }
339+ None
340+ }
341+
178342/// Check if a type is a primitive type
179343fn is_primitive_type ( ty : & Type ) -> bool {
180344 match ty {
@@ -1176,10 +1340,10 @@ pub fn build_operation_from_function(
11761340 // Check if it's a request body (Json<T>)
11771341 if let Some ( body) = parse_request_body ( input, known_schemas, struct_definitions) {
11781342 request_body = Some ( body) ;
1179- } else if let Some ( param ) =
1343+ } else if let Some ( params ) =
11801344 parse_function_parameter ( input, & path_params, known_schemas, struct_definitions)
11811345 {
1182- parameters. push ( param ) ;
1346+ parameters. extend ( params ) ;
11831347 }
11841348 }
11851349
0 commit comments