use std::collections::{HashMap, HashSet};
use crate::shared::cli_error::CliResult;
#[derive(Debug, Clone)]
pub struct TreeParser {
components: HashMap<String, ComponentEntry>,
}
#[derive(Debug, Clone)]
pub struct ComponentEntry {
pub name: String,
pub category: String,
pub dependencies: Vec<String>,
pub cargo_deps: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ResolvedSet {
pub components: HashSet<String>,
pub cargo_deps: HashSet<String>,
pub parent_dirs: HashSet<String>,
}
impl TreeParser {
pub fn parse_tree_md(content: &str) -> CliResult<Self> {
let mut components = HashMap::new();
let mut current_component: Option<ComponentEntry> = None;
let mut dependency_stack: Vec<String> = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("```") {
continue;
}
if let Some(line_content) = line.strip_prefix("* ") {
if let Some(component) = current_component.take() {
components.insert(component.name.clone(), component);
}
if let Some((name_part, category_part)) = line_content.rsplit_once(" (") {
let name = name_part.trim().to_string();
let category = category_part.trim_end_matches(')').to_string();
current_component = Some(ComponentEntry {
name: name.clone(),
category,
dependencies: Vec::new(),
cargo_deps: Vec::new(),
});
dependency_stack.clear();
dependency_stack.push(name);
}
}
else if let Some(dep_content) = line.strip_prefix("** ") {
if let Some(cargo_dep_name) = dep_content.strip_prefix("cargo: ") {
let cargo_dep = cargo_dep_name.trim().to_string();
if let Some(ref mut component) = current_component {
component.cargo_deps.push(cargo_dep);
}
} else if let Some((dep_name, _)) = dep_content.rsplit_once(" (") {
let dep_name = dep_name.trim().to_string();
if let Some(ref mut component) = current_component {
component.dependencies.push(dep_name.clone());
}
dependency_stack.truncate(1); dependency_stack.push(dep_name);
}
}
else if let Some(dep_content) = line.strip_prefix("*** ") {
if let Some(cargo_dep_name) = dep_content.strip_prefix("cargo: ") {
let cargo_dep = cargo_dep_name.trim().to_string();
if let Some(ref mut component) = current_component {
component.cargo_deps.push(cargo_dep);
}
} else if let Some((dep_name, _)) = dep_content.rsplit_once(" (") {
let dep_name = dep_name.trim().to_string();
if let Some(ref mut component) = current_component {
component.dependencies.push(dep_name);
}
}
}
}
if let Some(component) = current_component {
components.insert(component.name.clone(), component);
}
Ok(TreeParser { components })
}
pub fn get_all_component_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.components.keys().cloned().collect();
names.sort();
names
}
pub fn get_dependencies_map(&self) -> HashMap<String, Vec<String>> {
self.components
.iter()
.map(|(name, entry)| (name.clone(), entry.dependencies.clone()))
.collect()
}
pub fn resolve_dependencies(&self, user_components: &[String]) -> CliResult<ResolvedSet> {
let mut resolved_components = HashSet::new();
let mut resolved_cargo_deps = HashSet::new();
let mut resolved_parent_dirs = HashSet::new();
for component_name in user_components {
if let Some(component_entry) = self.components.get(component_name) {
resolved_components.insert(component_name.clone());
resolved_parent_dirs.insert(component_entry.category.clone());
for dep in &component_entry.dependencies {
resolved_components.insert(dep.clone());
if let Some(dep_entry) = self.components.get(dep) {
resolved_parent_dirs.insert(dep_entry.category.clone());
}
}
for cargo_dep in &component_entry.cargo_deps {
resolved_cargo_deps.insert(cargo_dep.clone());
}
} else {
println!("⚠️ Component '{}' not found in registry. Skipping...", component_name);
}
}
println!("📦 Final set of resolved components: {:?}", resolved_components);
println!("📦 Final set of cargo dependencies: {:?}", resolved_cargo_deps);
Ok(ResolvedSet {
components: resolved_components,
cargo_deps: resolved_cargo_deps,
parent_dirs: resolved_parent_dirs,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_TREE: &str = r#"
* button (ui)
** badge (ui)
** cargo: some-crate
* badge (ui)
* card (ui)
** button (ui)
*** badge (ui)
* demo_button (demos)
** button (ui)
"#;
#[test]
fn parse_tree_md_extracts_components() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
let names = parser.get_all_component_names();
assert!(names.contains(&"button".to_string()));
assert!(names.contains(&"badge".to_string()));
assert!(names.contains(&"card".to_string()));
assert!(names.contains(&"demo_button".to_string()));
}
#[test]
fn parse_tree_md_extracts_dependencies() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
let deps_map = parser.get_dependencies_map();
assert_eq!(deps_map.get("button").unwrap(), &vec!["badge".to_string()]);
assert!(deps_map.get("badge").unwrap().is_empty());
assert_eq!(deps_map.get("card").unwrap(), &vec!["button".to_string(), "badge".to_string()]);
}
#[test]
fn parse_tree_md_extracts_cargo_deps() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
let entry = parser.components.get("button").unwrap();
assert!(entry.cargo_deps.contains(&"some-crate".to_string()));
}
#[test]
fn parse_tree_md_extracts_category() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
assert_eq!(parser.components.get("button").unwrap().category, "ui");
assert_eq!(parser.components.get("demo_button").unwrap().category, "demos");
}
#[test]
fn get_all_component_names_sorted() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
let names = parser.get_all_component_names();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(names, sorted);
}
#[test]
fn resolve_dependencies_includes_transitive() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
let resolved = parser.resolve_dependencies(&["card".to_string()]).unwrap();
assert!(resolved.components.contains("card"));
assert!(resolved.components.contains("button"));
assert!(resolved.components.contains("badge"));
}
#[test]
fn resolve_dependencies_collects_parent_dirs() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
let resolved = parser.resolve_dependencies(&["demo_button".to_string()]).unwrap();
assert!(resolved.parent_dirs.contains("demos"));
assert!(resolved.parent_dirs.contains("ui"));
}
#[test]
fn resolve_dependencies_missing_component_skipped() {
let parser = TreeParser::parse_tree_md(SAMPLE_TREE).unwrap();
let resolved = parser.resolve_dependencies(&["nonexistent".to_string()]).unwrap();
assert!(resolved.components.is_empty());
}
}