@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
33use syn:: { FnArg , Pat , PatType , Type } ;
44use vespera_core:: {
55 route:: { Parameter , ParameterLocation } ,
6- schema:: { Schema , SchemaRef , SchemaType } ,
6+ schema:: { Schema , SchemaRef } ,
77} ;
88
99use 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.
2019fn 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}
0 commit comments