@@ -14,7 +14,7 @@ use crate::{
1414 metadata:: CollectedMetadata ,
1515 parser:: {
1616 build_operation_from_function, extract_default, extract_field_rename, extract_rename_all,
17- parse_enum_to_schema, parse_struct_to_schema, rename_field, strip_raw_prefix ,
17+ parse_enum_to_schema, parse_struct_to_schema, rename_field, strip_raw_prefix_owned ,
1818 } ,
1919 schema_macro:: type_utils:: get_type_default as utils_get_type_default,
2020} ;
@@ -103,13 +103,12 @@ pub fn generate_openapi_doc_with_metadata(
103103fn build_schema_lookups (
104104 metadata : & CollectedMetadata ,
105105) -> ( HashSet < String > , HashMap < String , String > ) {
106- let mut known_schema_names = HashSet :: new ( ) ;
107- let mut struct_definitions = HashMap :: new ( ) ;
106+ let mut known_schema_names = HashSet :: with_capacity ( metadata . structs . len ( ) ) ;
107+ let mut struct_definitions = HashMap :: with_capacity ( metadata . structs . len ( ) ) ;
108108
109109 for struct_meta in & metadata. structs {
110- let schema_name = struct_meta. name . clone ( ) ;
111- known_schema_names. insert ( schema_name) ;
112110 struct_definitions. insert ( struct_meta. name . clone ( ) , struct_meta. definition . clone ( ) ) ;
111+ known_schema_names. insert ( struct_meta. name . clone ( ) ) ;
113112 }
114113
115114 ( known_schema_names, struct_definitions)
@@ -139,7 +138,7 @@ fn build_file_cache(metadata: &CollectedMetadata) -> HashMap<String, syn::File>
139138/// Enables O(1) lookup of which file contains a given struct definition,
140139/// replacing the previous O(routes × file_read) linear scan.
141140fn build_struct_file_index ( file_cache : & HashMap < String , syn:: File > ) -> HashMap < String , & str > {
142- let mut index = HashMap :: new ( ) ;
141+ let mut index = HashMap :: with_capacity ( file_cache . len ( ) * 4 ) ;
143142 for ( path, ast) in file_cache {
144143 for item in & ast. items {
145144 if let syn:: Item :: Struct ( s) = item {
@@ -232,47 +231,63 @@ fn build_path_items(
232231 let mut paths = BTreeMap :: new ( ) ;
233232 let mut all_tags = BTreeSet :: new ( ) ;
234233
234+ // Pre-build function name index for O(1) lookup instead of O(items) per route
235+ let fn_index: HashMap < & str , HashMap < String , & syn:: ItemFn > > = file_cache
236+ . iter ( )
237+ . map ( |( path, ast) | {
238+ let fns: HashMap < String , & syn:: ItemFn > = ast
239+ . items
240+ . iter ( )
241+ . filter_map ( |item| {
242+ if let syn:: Item :: Fn ( fn_item) = item {
243+ Some ( ( fn_item. sig . ident . to_string ( ) , fn_item) )
244+ } else {
245+ None
246+ }
247+ } )
248+ . collect ( ) ;
249+ ( path. as_str ( ) , fns)
250+ } )
251+ . collect ( ) ;
252+
235253 for route_meta in & metadata. routes {
236- let Some ( file_ast ) = file_cache . get ( & route_meta. file_path ) else {
254+ let Some ( fns ) = fn_index . get ( route_meta. file_path . as_str ( ) ) else {
237255 continue ;
238256 } ;
239257
240- for item in & file_ast. items {
241- if let syn:: Item :: Fn ( fn_item) = item
242- && fn_item. sig . ident == route_meta. function_name
243- {
244- let Ok ( method) = HttpMethod :: try_from ( route_meta. method . as_str ( ) ) else {
245- eprintln ! (
246- "vespera: skipping route '{}' — unknown HTTP method '{}'" ,
247- route_meta. path, route_meta. method
248- ) ;
249- continue ;
250- } ;
258+ let Some ( fn_item) = fns. get ( & route_meta. function_name ) else {
259+ continue ;
260+ } ;
251261
252- if let Some ( tags) = & route_meta. tags {
253- for tag in tags {
254- all_tags. insert ( tag. clone ( ) ) ;
255- }
256- }
262+ let Ok ( method) = HttpMethod :: try_from ( route_meta. method . as_str ( ) ) else {
263+ eprintln ! (
264+ "vespera: skipping route '{}' — unknown HTTP method '{}'" ,
265+ route_meta. path, route_meta. method
266+ ) ;
267+ continue ;
268+ } ;
257269
258- let mut operation = build_operation_from_function (
259- & fn_item. sig ,
260- & route_meta. path ,
261- known_schema_names,
262- struct_definitions,
263- route_meta. error_status . as_deref ( ) ,
264- route_meta. tags . as_deref ( ) ,
265- ) ;
266- operation. description . clone_from ( & route_meta. description ) ;
267-
268- let path_item = paths
269- . entry ( route_meta. path . clone ( ) )
270- . or_insert_with ( PathItem :: default) ;
271-
272- path_item. set_operation ( method, operation) ;
273- break ;
270+ if let Some ( tags) = & route_meta. tags {
271+ for tag in tags {
272+ all_tags. insert ( tag. clone ( ) ) ;
274273 }
275274 }
275+
276+ let mut operation = build_operation_from_function (
277+ & fn_item. sig ,
278+ & route_meta. path ,
279+ known_schema_names,
280+ struct_definitions,
281+ route_meta. error_status . as_deref ( ) ,
282+ route_meta. tags . as_deref ( ) ,
283+ ) ;
284+ operation. description . clone_from ( & route_meta. description ) ;
285+
286+ let path_item = paths
287+ . entry ( route_meta. path . clone ( ) )
288+ . or_insert_with ( PathItem :: default) ;
289+
290+ path_item. set_operation ( method, operation) ;
276291 }
277292
278293 ( paths, all_tags)
@@ -321,7 +336,7 @@ fn process_default_functions(
321336 for field in & fields_named. named {
322337 let rust_field_name = field. ident . as_ref ( ) . map_or_else (
323338 || "unknown" . to_string ( ) ,
324- |i| strip_raw_prefix ( & i. to_string ( ) ) . to_string ( ) ,
339+ |i| strip_raw_prefix_owned ( i. to_string ( ) ) ,
325340 ) ;
326341 let field_name = extract_field_rename ( & field. attrs )
327342 . unwrap_or_else ( || rename_field ( & rust_field_name, struct_rename_all. as_deref ( ) ) ) ;
@@ -1663,4 +1678,30 @@ pub fn create_users() -> String {
16631678 panic ! ( "Expected inline schema with default" ) ;
16641679 }
16651680 }
1681+
1682+ #[ test]
1683+ fn test_generate_openapi_route_function_not_in_ast ( ) {
1684+ let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
1685+ let route_content = "pub fn get_items() -> String { \" items\" .to_string() }\n " ;
1686+ let route_file = create_temp_file ( & temp_dir, "users.rs" , route_content) ;
1687+
1688+ let mut metadata = CollectedMetadata :: new ( ) ;
1689+ metadata. routes . push ( RouteMetadata {
1690+ method : "GET" . to_string ( ) ,
1691+ path : "/users" . to_string ( ) ,
1692+ function_name : "get_users" . to_string ( ) ,
1693+ module_path : "test::users" . to_string ( ) ,
1694+ file_path : route_file. to_string_lossy ( ) . to_string ( ) ,
1695+ signature : "fn get_users() -> String" . to_string ( ) ,
1696+ error_status : None ,
1697+ tags : None ,
1698+ description : None ,
1699+ } ) ;
1700+
1701+ let doc = generate_openapi_doc_with_metadata ( None , None , None , & metadata, None ) ;
1702+ assert ! (
1703+ doc. paths. is_empty( ) ,
1704+ "Route with non-matching function should be skipped"
1705+ ) ;
1706+ }
16661707}
0 commit comments