Skip to content

Commit 5ea33c6

Browse files
committed
Fix query
1 parent 2586519 commit 5ea33c6

3 files changed

Lines changed: 286 additions & 27 deletions

File tree

crates/vespera_macro/src/parser.rs

Lines changed: 178 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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
2931
pub 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
179343
fn 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

Comments
 (0)