use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use vespera_core::route::{HttpMethod, PathItem};
use crate::{
file_utils::normalize_path_key,
metadata::CollectedMetadata,
parser::{OperationRouteConfig, build_operation_from_function},
route_impl::StoredRouteInfo,
};
type FnIndex<'a> = HashMap<String, HashMap<String, &'a syn::ItemFn>>;
type StorageFnSigs<'a> = HashMap<(Option<String>, &'a str), Option<&'a str>>;
pub(super) fn build_path_items(
metadata: &CollectedMetadata,
known_schema_names: &HashSet<&str>,
struct_definitions: &HashMap<&str, &str>,
file_cache: &HashMap<String, syn::File>,
route_storage: &[StoredRouteInfo],
) -> syn::Result<(BTreeMap<String, PathItem>, BTreeSet<String>)> {
let mut paths = BTreeMap::new();
let mut all_tags = BTreeSet::new();
let cwd = std::env::current_dir().unwrap_or_default();
let fn_index: FnIndex<'_> = file_cache
.iter()
.map(|(path, ast)| {
let fns: HashMap<String, &syn::ItemFn> = ast
.items
.iter()
.filter_map(|item| {
if let syn::Item::Fn(fn_item) = item {
Some((fn_item.sig.ident.to_string(), fn_item))
} else {
None
}
})
.collect();
(normalize_path_key(path, &cwd), fns)
})
.collect();
let storage_fn_sigs = build_storage_fn_sigs(route_storage, &fn_index, &cwd);
let mut parallel_jobs: Vec<(usize, &crate::metadata::RouteMetadata, &str)> = Vec::new();
let mut ast_jobs: Vec<(usize, &crate::metadata::RouteMetadata, &syn::Signature)> = Vec::new();
for (idx, route_meta) in metadata.routes.iter().enumerate() {
let storage_key = (
Some(normalize_path_key(&route_meta.file_path, &cwd)),
route_meta.function_name.as_str(),
);
let legacy_storage_key = (None, route_meta.function_name.as_str());
if let Some(fn_sig_str) = storage_fn_sigs
.get(&storage_key)
.copied()
.flatten()
.or_else(|| storage_fn_sigs.get(&legacy_storage_key).copied().flatten())
{
parallel_jobs.push((idx, route_meta, fn_sig_str));
} else if let Some(norm_key) = storage_key.0
&& let Some(fns) = fn_index.get(&norm_key)
&& let Some(fn_item) = fns.get(&route_meta.function_name)
{
ast_jobs.push((idx, route_meta, &fn_item.sig));
}
}
let build_one = |route_meta: &crate::metadata::RouteMetadata,
fn_sig: &syn::Signature|
-> syn::Result<Option<(HttpMethod, vespera_core::route::Operation)>> {
let Ok(method) = HttpMethod::try_from(route_meta.method.as_str()) else {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"vespera: route '{}' has unsupported HTTP method '{}'. Supported methods are GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.",
route_meta.path, route_meta.method
),
));
};
let mut operation = build_operation_from_function(
fn_sig,
&route_meta.path,
known_schema_names,
struct_definitions,
OperationRouteConfig {
error_status: route_meta.error_status.as_deref(),
typed_responses: route_meta.typed_responses.as_deref(),
success_status: route_meta.success_status,
tags: route_meta.tags.as_deref(),
security: route_meta.security.as_deref(),
headers: Some(&route_meta.headers),
operation_id: route_meta.operation_id.as_deref(),
summary: route_meta.summary.as_deref(),
request_example: route_meta.request_example.as_ref(),
response_example: route_meta.response_example.as_ref(),
deprecated: route_meta.deprecated,
},
);
operation.description.clone_from(&route_meta.description);
Ok(Some((method, operation)))
};
let mut results: Vec<(usize, HttpMethod, vespera_core::route::Operation)> =
run_route_jobs_parallel(¶llel_jobs, &build_one)?;
for (idx, route_meta, fn_sig) in ast_jobs {
if let Some((method, operation)) = build_one(route_meta, fn_sig)? {
results.push((idx, method, operation));
}
}
results.sort_unstable_by_key(|(idx, _, _)| *idx);
assemble_path_items(results, metadata, &mut paths, &mut all_tags)?;
Ok((paths, all_tags))
}
fn assemble_path_items(
results: Vec<(usize, HttpMethod, vespera_core::route::Operation)>,
metadata: &CollectedMetadata,
paths: &mut BTreeMap<String, PathItem>,
all_tags: &mut BTreeSet<String>,
) -> syn::Result<()> {
let mut claimed: HashMap<(&str, HttpMethod), &str> = HashMap::new();
for (idx, method, operation) in results {
let route_meta = &metadata.routes[idx];
if let Some(tags) = &route_meta.tags {
for tag in tags {
all_tags.insert(tag.clone());
}
}
let path_item = paths.entry(route_meta.path.clone()).or_default();
if path_item.try_set_operation(method, operation).is_some() {
let previous = claimed
.get(&(route_meta.path.as_str(), method))
.copied()
.unwrap_or("<unknown>");
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"duplicate route: `{method} {path}` is defined by both `{previous}` and \
`{current}` — each (method, path) pair must map to exactly one handler",
path = route_meta.path,
current = route_meta.function_name,
),
));
}
claimed.insert(
(route_meta.path.as_str(), method),
route_meta.function_name.as_str(),
);
}
Ok(())
}
fn build_storage_fn_sigs<'a>(
route_storage: &'a [StoredRouteInfo],
fn_index: &FnIndex<'_>,
cwd: &std::path::Path,
) -> StorageFnSigs<'a> {
let mut storage = HashMap::with_capacity(route_storage.len());
for s in route_storage {
let norm_fp = s.file_path.as_deref().map(|fp| normalize_path_key(fp, cwd));
let already_in_ast = norm_fp
.as_ref()
.and_then(|fp| fn_index.get(fp))
.is_some_and(|fns| fns.contains_key(&s.fn_name));
if already_in_ast {
continue;
}
let key = (norm_fp, s.fn_name.as_str());
storage
.entry(key)
.and_modify(|slot| *slot = None)
.or_insert(Some(s.fn_sig_str.as_str()));
}
storage
}
pub(super) const PARALLEL_THRESHOLD: usize = 16;
pub(super) type RouteJob<'a> = (usize, &'a crate::metadata::RouteMetadata, &'a str);
pub(super) type BuiltOperation = (usize, HttpMethod, vespera_core::route::Operation);
pub(super) type OperationBuilder<'a> = dyn Fn(
&crate::metadata::RouteMetadata,
&syn::Signature,
) -> syn::Result<Option<(HttpMethod, vespera_core::route::Operation)>>
+ Sync
+ 'a;
struct FallbackGuard;
impl Drop for FallbackGuard {
fn drop(&mut self) {
proc_macro2::fallback::unforce();
}
}
fn run_route_jobs_parallel(
jobs: &[RouteJob<'_>],
build_one: &OperationBuilder<'_>,
) -> syn::Result<Vec<BuiltOperation>> {
parallel_filter_map(jobs, &|&(idx, route_meta, fn_sig_str): &RouteJob<'_>| {
let fn_sig = syn::parse_str::<syn::Signature>(fn_sig_str).map_err(|err| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"vespera: failed to parse stored signature for route '{}': {err}",
route_meta.path
),
)
})?;
Ok(build_one(route_meta, &fn_sig)?.map(|(m, op)| (idx, m, op)))
})
}
pub(super) fn parallel_filter_map<T: Sync, R: Send>(
jobs: &[T],
f: &(dyn Fn(&T) -> syn::Result<Option<R>> + Sync),
) -> syn::Result<Vec<R>> {
let workers = std::thread::available_parallelism()
.map_or(1, std::num::NonZero::get)
.min(jobs.len().div_ceil(PARALLEL_THRESHOLD));
if workers <= 1 || jobs.len() < PARALLEL_THRESHOLD {
return jobs.iter().filter_map(|job| f(job).transpose()).collect();
}
proc_macro2::fallback::force();
let _guard = FallbackGuard;
let chunk_size = jobs.len().div_ceil(workers);
std::thread::scope(|scope| {
let handles: Vec<_> = jobs
.chunks(chunk_size)
.map(|chunk| {
scope.spawn(move || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
chunk.iter().filter_map(|job| f(job).transpose()).collect()
}))
})
})
.collect();
let mut results: Vec<R> = Vec::with_capacity(jobs.len());
for handle in handles {
let worker_result = handle
.join()
.map_err(|panic| worker_panic_error(panic.as_ref()))?;
let chunk_results: syn::Result<Vec<R>> =
worker_result.map_err(|panic| worker_panic_error(panic.as_ref()))?;
results.extend(chunk_results?);
}
Ok(results)
})
}
fn worker_panic_error(panic: &(dyn std::any::Any + Send)) -> syn::Error {
let message = panic.downcast_ref::<&str>().map_or_else(
|| {
panic.downcast_ref::<String>().map_or_else(
|| "parallel macro worker panicked".to_string(),
std::clone::Clone::clone,
)
},
|message| (*message).to_string(),
);
syn::Error::new(
proc_macro2::Span::call_site(),
format!("vespera: parallel OpenAPI worker failed: {message}"),
)
}
#[cfg(test)]
mod tests;