Skip to content

Commit 8ca1219

Browse files
authored
Merge pull request #24 from dev-five-git/fix-query-issue
Fix query issue
2 parents 2586519 + f150b22 commit 8ca1219

File tree

5 files changed

+413
-39
lines changed

5 files changed

+413
-39
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespera/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch"},"note":"Fix query issue","date":"2025-12-04T10:43:01.304353300Z"}

crates/vespera_macro/src/parser.rs

Lines changed: 237 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,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
179402
fn 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

Comments
 (0)