Skip to content

Commit 21947f6

Browse files
authored
Merge pull request #82 from dev-five-git/support-enum-in-query
Support enum in query
2 parents 16e8bca + 322f541 commit 21947f6

6 files changed

Lines changed: 334 additions & 27 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"Cargo.toml":"Patch"},"note":"Support enum in query","date":"2026-02-18T10:50:32.672936100Z"}

crates/vespera_macro/src/parser/parameters.rs

Lines changed: 156 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
33
use syn::{FnArg, Pat, PatType, Type};
44
use vespera_core::{
55
route::{Parameter, ParameterLocation},
6-
schema::{Schema, SchemaRef, SchemaType},
6+
schema::{Schema, SchemaRef},
77
};
88

99
use super::schema::{
@@ -14,9 +14,8 @@ use crate::schema_macro::type_utils::{
1414
is_map_type as utils_is_map_type, is_primitive_like as utils_is_primitive_like,
1515
};
1616

17-
/// Convert `SchemaRef` to inline schema for query parameters
18-
/// Query parameters should always use inline schemas, not refs
19-
/// Adds nullable flag if the field is optional
17+
/// Convert `SchemaRef` for query parameters, adding nullable flag if optional.
18+
/// Preserves `$ref` for known types (e.g. enums) — only wraps with nullable when optional.
2019
fn convert_to_inline_schema(field_schema: SchemaRef, is_optional: bool) -> SchemaRef {
2120
match field_schema {
2221
SchemaRef::Inline(mut schema) => {
@@ -25,12 +24,17 @@ fn convert_to_inline_schema(field_schema: SchemaRef, is_optional: bool) -> Schem
2524
}
2625
SchemaRef::Inline(schema)
2726
}
28-
SchemaRef::Ref(_) => {
29-
let mut schema = Schema::new(SchemaType::Object);
27+
SchemaRef::Ref(r) => {
3028
if is_optional {
31-
schema.nullable = Some(true);
29+
SchemaRef::Inline(Box::new(Schema {
30+
ref_path: Some(r.ref_path),
31+
schema_type: None,
32+
nullable: Some(true),
33+
..Default::default()
34+
}))
35+
} else {
36+
SchemaRef::Ref(r)
3237
}
33-
SchemaRef::Inline(Box::new(schema))
3438
}
3539
}
3640
}
@@ -433,7 +437,7 @@ mod tests {
433437
use insta::{assert_debug_snapshot, with_settings};
434438
use rstest::rstest;
435439
use vespera_core::route::ParameterLocation;
436-
use vespera_core::schema::Reference;
440+
use vespera_core::schema::{Reference, SchemaType};
437441

438442
use super::*;
439443

@@ -1011,16 +1015,11 @@ mod tests {
10111015
}
10121016

10131017
#[test]
1014-
fn test_schema_ref_to_inline_conversion_required() {
1015-
// Test line 318: SchemaRef::Ref converted to inline for required fields
1016-
// This requires a field where:
1017-
// 1. field_schema is SchemaRef::Ref
1018-
// 2. is_optional is false
1019-
// 3. The ref conversion at lines 294-304 fails (no struct_def)
1018+
fn test_schema_ref_preserved_for_required_field() {
1019+
// Required field with known schema but no struct definition → $ref preserved
10201020
let mut struct_definitions = HashMap::new();
10211021
let mut known_schemas = HashSet::new();
10221022

1023-
// Struct with required RefType field
10241023
struct_definitions.insert(
10251024
"QueryWithRef".to_string(),
10261025
r"
@@ -1032,7 +1031,7 @@ mod tests {
10321031
);
10331032

10341033
// RefType is a known schema (will generate SchemaRef::Ref)
1035-
// BUT we don't have its struct definition, so the conversion at 296-303 fails
1034+
// No struct definition, so ref stays as-is (e.g. enum type)
10361035
known_schemas.insert("RefType".to_string());
10371036

10381037
let ty: Type = syn::parse_str("QueryWithRef").unwrap();
@@ -1041,12 +1040,12 @@ mod tests {
10411040
assert!(result.is_some());
10421041
let params = result.unwrap();
10431042
assert_eq!(params.len(), 1);
1044-
// Line 318: Ref that couldn't be converted is turned into inline object
1043+
// $ref is preserved for required fields
10451044
match &params[0].schema {
1046-
Some(SchemaRef::Inline(schema)) => {
1047-
assert_eq!(schema.schema_type, Some(SchemaType::Object));
1045+
Some(SchemaRef::Ref(r)) => {
1046+
assert_eq!(r.ref_path, "#/components/schemas/RefType");
10481047
}
1049-
_ => panic!("Expected inline schema (converted from Ref)"),
1048+
_ => panic!("Expected $ref schema for required known type"),
10501049
}
10511050
}
10521051

@@ -1122,30 +1121,161 @@ mod tests {
11221121
}
11231122

11241123
#[test]
1125-
fn test_convert_to_inline_schema_with_ref_optional() {
1124+
fn test_convert_to_inline_schema_ref_optional_preserves_ref_path() {
11261125
let schema = SchemaRef::Ref(Reference {
11271126
ref_path: "#/components/schemas/User".to_string(),
11281127
});
11291128
let result = convert_to_inline_schema(schema, true);
11301129
match result {
11311130
SchemaRef::Inline(s) => {
1131+
assert_eq!(s.ref_path, Some("#/components/schemas/User".to_string()));
11321132
assert_eq!(s.nullable, Some(true));
1133+
assert_eq!(s.schema_type, None);
11331134
}
1134-
SchemaRef::Ref(_) => panic!("Expected Inline"),
1135+
SchemaRef::Ref(_) => panic!("Expected Inline wrapper for optional $ref"),
11351136
}
11361137
}
11371138

11381139
#[test]
1139-
fn test_convert_to_inline_schema_ref_optional() {
1140+
fn test_convert_to_inline_schema_ref_required_passes_through() {
1141+
use vespera_core::schema::Reference;
1142+
let schema = SchemaRef::Ref(Reference::schema("SomeType"));
1143+
let result = convert_to_inline_schema(schema, false);
1144+
match result {
1145+
SchemaRef::Ref(r) => {
1146+
assert_eq!(r.ref_path, "#/components/schemas/SomeType");
1147+
}
1148+
SchemaRef::Inline(_) => panic!("Expected $ref pass-through for required field"),
1149+
}
1150+
}
1151+
1152+
#[test]
1153+
fn test_convert_to_inline_schema_ref_optional_wraps_nullable() {
11401154
use vespera_core::schema::Reference;
11411155
let schema = SchemaRef::Ref(Reference::schema("SomeType"));
11421156
let result = convert_to_inline_schema(schema, true);
11431157
match result {
11441158
SchemaRef::Inline(s) => {
1145-
assert_eq!(s.schema_type, Some(SchemaType::Object));
1159+
assert_eq!(
1160+
s.ref_path,
1161+
Some("#/components/schemas/SomeType".to_string())
1162+
);
11461163
assert_eq!(s.nullable, Some(true));
11471164
}
1148-
SchemaRef::Ref(_) => panic!("Expected Inline"),
1165+
SchemaRef::Ref(_) => panic!("Expected Inline wrapper for optional $ref"),
1166+
}
1167+
}
1168+
1169+
// ======== Enum query parameter tests ========
1170+
1171+
#[test]
1172+
fn test_query_struct_with_enum_field_produces_ref() {
1173+
// Enum field in a query struct should produce $ref to the enum schema
1174+
let mut struct_definitions = HashMap::new();
1175+
let mut known_schemas = HashSet::new();
1176+
1177+
struct_definitions.insert(
1178+
"FilterParams".to_string(),
1179+
r"
1180+
pub struct FilterParams {
1181+
pub status: Status,
1182+
pub page: i32,
1183+
}
1184+
"
1185+
.to_string(),
1186+
);
1187+
1188+
// Status is a known enum schema (registered via #[derive(Schema)])
1189+
// Its definition is an enum, so ItemStruct parsing will fail → $ref preserved
1190+
known_schemas.insert("Status".to_string());
1191+
struct_definitions.insert(
1192+
"Status".to_string(),
1193+
r"
1194+
pub enum Status {
1195+
Active,
1196+
Inactive,
1197+
Pending,
1198+
}
1199+
"
1200+
.to_string(),
1201+
);
1202+
1203+
let ty: Type = syn::parse_str("FilterParams").unwrap();
1204+
let result = parse_query_struct_to_parameters(&ty, &known_schemas, &struct_definitions);
1205+
1206+
assert!(result.is_some());
1207+
let params = result.unwrap();
1208+
assert_eq!(params.len(), 2);
1209+
1210+
// First param: status → $ref to enum schema
1211+
assert_eq!(params[0].name, "status");
1212+
assert_eq!(params[0].r#in, ParameterLocation::Query);
1213+
assert_eq!(params[0].required, Some(true));
1214+
match &params[0].schema {
1215+
Some(SchemaRef::Ref(r)) => {
1216+
assert_eq!(r.ref_path, "#/components/schemas/Status");
1217+
}
1218+
_ => panic!(
1219+
"Expected $ref for enum query parameter, got: {:?}",
1220+
params[0].schema
1221+
),
1222+
}
1223+
1224+
// Second param: page → inline integer
1225+
assert_eq!(params[1].name, "page");
1226+
assert_eq!(params[1].required, Some(true));
1227+
match &params[1].schema {
1228+
Some(SchemaRef::Inline(s)) => {
1229+
assert_eq!(s.schema_type, Some(SchemaType::Integer));
1230+
}
1231+
_ => panic!("Expected inline integer schema"),
1232+
}
1233+
}
1234+
1235+
#[test]
1236+
fn test_query_struct_with_optional_enum_field() {
1237+
// Option<Enum> field → nullable $ref
1238+
let mut struct_definitions = HashMap::new();
1239+
let mut known_schemas = HashSet::new();
1240+
1241+
struct_definitions.insert(
1242+
"FilterParams".to_string(),
1243+
r"
1244+
pub struct FilterParams {
1245+
pub status: Option<Status>,
1246+
}
1247+
"
1248+
.to_string(),
1249+
);
1250+
1251+
known_schemas.insert("Status".to_string());
1252+
struct_definitions.insert(
1253+
"Status".to_string(),
1254+
r"
1255+
pub enum Status {
1256+
Active,
1257+
Inactive,
1258+
}
1259+
"
1260+
.to_string(),
1261+
);
1262+
1263+
let ty: Type = syn::parse_str("FilterParams").unwrap();
1264+
let result = parse_query_struct_to_parameters(&ty, &known_schemas, &struct_definitions);
1265+
1266+
assert!(result.is_some());
1267+
let params = result.unwrap();
1268+
assert_eq!(params.len(), 1);
1269+
assert_eq!(params[0].name, "status");
1270+
assert_eq!(params[0].required, Some(false));
1271+
1272+
// Option<Enum> → inline schema with ref_path + nullable
1273+
match &params[0].schema {
1274+
Some(SchemaRef::Inline(s)) => {
1275+
assert_eq!(s.ref_path, Some("#/components/schemas/Status".to_string()));
1276+
assert_eq!(s.nullable, Some(true));
1277+
}
1278+
_ => panic!("Expected inline schema with ref_path and nullable for Option<Enum>"),
11491279
}
11501280
}
11511281
}

examples/axum-example/openapi.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,36 @@
13041304
}
13051305
}
13061306
},
1307+
"/terms": {
1308+
"get": {
1309+
"operationId": "list_terms",
1310+
"tags": [
1311+
"terms"
1312+
],
1313+
"parameters": [
1314+
{
1315+
"name": "termsType",
1316+
"in": "query",
1317+
"required": true,
1318+
"schema": {
1319+
"$ref": "#/components/schemas/TermsType"
1320+
}
1321+
}
1322+
],
1323+
"responses": {
1324+
"200": {
1325+
"description": "Successful response",
1326+
"content": {
1327+
"application/json": {
1328+
"schema": {
1329+
"$ref": "#/components/schemas/TermsQuery"
1330+
}
1331+
}
1332+
}
1333+
}
1334+
}
1335+
}
1336+
},
13071337
"/test-struct": {
13081338
"get": {
13091339
"operationId": "mod_file_with_test_struct",
@@ -3300,6 +3330,24 @@
33003330
"isSubscribed"
33013331
]
33023332
},
3333+
"TermsQuery": {
3334+
"type": "object",
3335+
"properties": {
3336+
"termsType": {
3337+
"$ref": "#/components/schemas/TermsType"
3338+
}
3339+
},
3340+
"required": [
3341+
"termsType"
3342+
]
3343+
},
3344+
"TermsType": {
3345+
"type": "string",
3346+
"enum": [
3347+
"terms",
3348+
"privacy"
3349+
]
3350+
},
33033351
"TestStruct": {
33043352
"type": "object",
33053353
"properties": {
@@ -3605,6 +3653,9 @@
36053653
{
36063654
"name": "hello"
36073655
},
3656+
{
3657+
"name": "terms"
3658+
},
36083659
{
36093660
"name": "typed-form"
36103661
},

examples/axum-example/src/routes/mod.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::collections::HashMap;
22

3-
use serde::Deserialize;
3+
use sea_orm::{DeriveActiveEnum, EnumIter};
4+
use serde::{Deserialize, Serialize};
45
use vespera::{
56
Schema,
67
axum::{Json, extract::Query},
@@ -170,3 +171,25 @@ pub async fn mod_file_with_complex_struct_body_with_rename(
170171
pub async fn mod_file_with_test_struct(Query(query): Query<TestStruct>) -> Json<TestStruct> {
171172
Json(query)
172173
}
174+
#[derive(
175+
Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, vespera::Schema,
176+
)]
177+
#[serde(rename_all = "camelCase")]
178+
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "terms_terms_type")]
179+
pub enum TermsType {
180+
#[sea_orm(string_value = "terms")]
181+
Terms,
182+
#[sea_orm(string_value = "privacy")]
183+
Privacy,
184+
}
185+
186+
#[derive(Debug, Serialize, Deserialize, Schema)]
187+
#[serde(rename_all = "camelCase")]
188+
pub struct TermsQuery {
189+
pub terms_type: TermsType,
190+
}
191+
192+
#[vespera::route(get, path = "/terms", tags = ["terms"])]
193+
pub async fn list_terms(Query(query): Query<TermsQuery>) -> Json<TermsQuery> {
194+
Json(query)
195+
}

0 commit comments

Comments
 (0)