use std::path::{Path, PathBuf};
use serde::Serialize;
use walkdir::WalkDir;
#[derive(Debug, Clone, Serialize)]
pub struct DiscoveredRoute {
pub file: PathBuf,
pub pattern: String,
pub module: String,
pub is_layout: bool,
}
pub fn discover<P: AsRef<Path>>(routes_root: P) -> Vec<DiscoveredRoute> {
let root = routes_root.as_ref();
let mut out = Vec::new();
if !root.exists() {
return out;
}
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if !path.is_file() {
continue;
}
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
if ext != "rs" {
continue;
}
let rel = match path.strip_prefix(root) {
Ok(r) => r.to_path_buf(),
Err(_) => continue,
};
if let Some(route) = parse_route(rel.clone(), path.to_path_buf()) {
out.push(route);
}
}
out.sort_by(|a, b| a.pattern.cmp(&b.pattern));
out
}
pub fn layout_chain_for(page_pattern: &str, layouts: &[(String, PathBuf)]) -> Vec<String> {
let mut chain: Vec<String> = layouts
.iter()
.filter(|(pat, _)| layout_applies(pat, page_pattern))
.map(|(pat, _)| pat.clone())
.collect();
chain.sort_by_key(|p| p.len());
chain
}
fn layout_applies(layout_pattern: &str, page_pattern: &str) -> bool {
if layout_pattern == "/" {
return true;
}
if page_pattern == layout_pattern {
return true;
}
page_pattern.starts_with(layout_pattern)
&& page_pattern
.as_bytes()
.get(layout_pattern.len())
.is_some_and(|b| *b == b'/')
}
fn parse_route(rel: PathBuf, abs: PathBuf) -> Option<DiscoveredRoute> {
let stem = rel.file_stem()?.to_str()?;
if stem == "mod" || stem == "_registry" {
return None;
}
let parent = rel.parent().unwrap_or(Path::new("")).to_path_buf();
let is_layout = stem == "layout" || stem == "_layout";
let mut segments: Vec<String> = parent
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.filter(|s| !s.is_empty())
.collect();
if !is_layout && stem != "index" {
segments.push(stem.to_string());
}
let pattern = if segments.is_empty() {
"/".to_string()
} else {
let url_segments: Vec<String> = segments.iter().map(|s| convert_segment(s)).collect();
format!("/{}", url_segments.join("/"))
};
let module = if segments.is_empty() {
"index".to_string()
} else {
segments
.iter()
.map(|s| s.replace(['[', ']'], "_").replace("...", "rest_"))
.collect::<Vec<_>>()
.join("::")
};
Some(DiscoveredRoute {
file: abs,
pattern,
module,
is_layout,
})
}
fn convert_segment(seg: &str) -> String {
if let Some(inner) = seg.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
if let Some(rest) = inner.strip_prefix("...") {
return format!("*{}", rest);
}
return format!(":{}", inner);
}
seg.to_string()
}