Skip to content

Commit 9869a40

Browse files
authored
Merge pull request #88 from dev-five-git/improve-performance
Improve performance
2 parents 116aed5 + f77d3d1 commit 9869a40

28 files changed

Lines changed: 1464 additions & 1073 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"Cargo.toml":"Patch"},"note":"Optimize","date":"2026-02-27T13:50:39.128469700Z"}

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespera_macro/src/collector.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ pub fn collect_metadata(
2222
folder_name: &str,
2323
) -> MacroResult<(CollectedMetadata, HashMap<String, syn::File>)> {
2424
let mut metadata = CollectedMetadata::new();
25-
let mut file_asts = HashMap::new();
2625

2726
let files = collect_files(folder_path).map_err(|e| err_call_site(format!("vespera! macro: failed to scan route folder '{}': {}. Verify the folder exists and is readable.", folder_path.display(), e)))?;
2827

28+
let mut file_asts = HashMap::with_capacity(files.len());
29+
2930
for file in files {
3031
if file.extension().is_none_or(|e| e != "rs") {
3132
continue;
@@ -42,8 +43,9 @@ pub fn collect_metadata(
4243
let file_ast = syn::parse_file(&content).map_err(|e| err_call_site(format!("vespera! macro: syntax error in '{}': {}. Fix the Rust syntax errors in this file.", file.display(), e)))?;
4344

4445
// Store file AST for downstream reuse (keyed by display path to match RouteMetadata.file_path)
45-
let file_path_key = file.display().to_string();
46-
file_asts.insert(file_path_key, file_ast.clone());
46+
let file_path = file.display().to_string();
47+
file_asts.insert(file_path.clone(), file_ast);
48+
let file_ast = &file_asts[&file_path];
4749

4850
// Get module path
4951
let segments = file
@@ -64,8 +66,6 @@ pub fn collect_metadata(
6466
format!("{}::{}", folder_name, segments.join("::"))
6567
};
6668

67-
let file_path = file.display().to_string();
68-
6969
// Pre-compute base path once per file (avoids repeated segments.join per route)
7070
let base_path = format!("/{}", segments.join("/"));
7171

crates/vespera_macro/src/file_utils.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ use std::{
33
path::{Path, PathBuf},
44
};
55

6-
/// Read and parse a Rust source file, returning None on error (silent).
7-
pub fn try_read_and_parse_file(path: &Path) -> Option<syn::File> {
8-
let content = std::fs::read_to_string(path).ok()?;
9-
syn::parse_file(&content).ok()
10-
}
11-
126
/// Read and parse a Rust source file, printing warnings on error.
137
#[allow(clippy::similar_names)]
148
pub fn read_and_parse_file_warn(path: &Path, context: &str) -> Option<syn::File> {

crates/vespera_macro/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ mod http;
4848
mod metadata;
4949
mod method;
5050
mod openapi_generator;
51-
mod parse_utils;
51+
5252
mod parser;
5353
mod route;
5454
mod route_impl;

crates/vespera_macro/src/openapi_generator.rs

Lines changed: 81 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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(
103103
fn 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.
141140
fn 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

Comments
 (0)