use super::{ConditionalBranch, IRNode, RouterRoute};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiscoveredRoute {
pub path: String,
pub element_names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiscoveredRouter {
pub module_scope: Option<String>,
pub routes: Vec<DiscoveredRoute>,
}
pub fn discover_routers(ir: &IRNode) -> Vec<DiscoveredRouter> {
let mut out = Vec::new();
walk(ir, &mut out);
out
}
fn walk(ir: &IRNode, out: &mut Vec<DiscoveredRouter>) {
match ir {
IRNode::Router {
routes,
fallback,
module_scope,
..
} => {
let mut discovered = DiscoveredRouter {
module_scope: module_scope.clone(),
routes: Vec::with_capacity(routes.len()),
};
for route in routes {
discovered.routes.push(DiscoveredRoute {
path: route.path.clone(),
element_names: collect_element_names(&route.children),
});
}
out.push(discovered);
for route in routes {
for child in &route.children {
walk(child, out);
}
}
if let Some(fb) = fallback {
for child in fb {
walk(child, out);
}
}
}
IRNode::Element(el) => {
for child in &el.ir_children {
walk(child, out);
}
}
IRNode::ForEach { template, .. } => {
for child in template {
walk(child, out);
}
}
IRNode::Conditional {
branches, fallback, ..
} => {
for branch in branches {
for child in &branch.children {
walk(child, out);
}
}
if let Some(fb) = fallback {
for child in fb {
walk(child, out);
}
}
}
}
}
fn collect_element_names(children: &[IRNode]) -> Vec<String> {
let mut names = Vec::new();
let mut queue: std::collections::VecDeque<&IRNode> = children.iter().collect();
while let Some(node) = queue.pop_front() {
match node {
IRNode::Element(el) => {
names.push(el.element_type.clone());
for child in &el.ir_children {
queue.push_back(child);
}
}
IRNode::ForEach { template, .. } => {
for child in template {
queue.push_back(child);
}
}
IRNode::Conditional {
branches, fallback, ..
} => {
for ConditionalBranch { children, .. } in branches {
for child in children {
queue.push_back(child);
}
}
if let Some(fb) = fallback {
for child in fb {
queue.push_back(child);
}
}
}
IRNode::Router {
routes, fallback, ..
} => {
for RouterRoute { children, .. } in routes {
for child in children {
queue.push_back(child);
}
}
if let Some(fb) = fallback {
for child in fb {
queue.push_back(child);
}
}
}
}
}
names
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::ast_to_ir_node;
fn parse(source: &str) -> IRNode {
let doc = hypen_parser::parse_document(source).expect("parse");
let component = doc.components.first().expect("has component");
ast_to_ir_node(component)
}
#[test]
fn discovers_flat_router() {
let ir = parse(
r#"
module App {
Router {
Route(path: "/") { HomePage() }
Route(path: "/search") { Search() }
}
}
"#,
);
let routers = discover_routers(&ir);
assert_eq!(routers.len(), 1);
let r = &routers[0];
assert_eq!(r.module_scope.as_deref(), Some("app"));
assert_eq!(r.routes.len(), 2);
assert_eq!(r.routes[0].path, "/");
assert_eq!(r.routes[0].element_names, vec!["HomePage"]);
assert_eq!(r.routes[1].path, "/search");
assert_eq!(r.routes[1].element_names, vec!["Search"]);
}
#[test]
fn discovers_component_through_wrappers() {
let ir = parse(
r#"
module App {
Router {
Route(path: "/") {
Column {
HomePage()
BottomNav()
}
}
}
}
"#,
);
let routers = discover_routers(&ir);
assert_eq!(routers.len(), 1);
assert_eq!(
routers[0].routes[0].element_names,
vec!["Column", "HomePage", "BottomNav"]
);
}
#[test]
fn discovers_route_params() {
let ir = parse(
r#"
module App {
Router {
Route(path: "/user-profile/:id") { UserProfile() }
Route(path: "/comments/:postId") { Comments() }
}
}
"#,
);
let routers = discover_routers(&ir);
assert_eq!(routers[0].routes[0].path, "/user-profile/:id");
assert_eq!(routers[0].routes[1].path, "/comments/:postId");
}
#[test]
fn discovers_nested_routers() {
let ir = parse(
r#"
module App {
Router {
Route(path: "/") {
Column {
Home()
Router {
Route(path: "/") { Feed() }
Route(path: "/explore") { Explore() }
}
}
}
}
}
"#,
);
let routers = discover_routers(&ir);
assert_eq!(routers.len(), 2);
assert_eq!(routers[0].routes[0].path, "/");
assert_eq!(routers[1].routes.len(), 2);
assert_eq!(routers[1].routes[0].path, "/");
assert_eq!(routers[1].routes[1].path, "/explore");
}
#[test]
fn no_routers_returns_empty() {
let ir = parse(
r#"
module App {
Column {
Text("No routes here")
Button("Click me")
}
}
"#,
);
assert!(discover_routers(&ir).is_empty());
}
}