use anyhow::{bail, Result};
use crate::cli::OutputConfig;
use crate::tags::{OntologyConfig, TagsConfig};
use super::{ExpandArgs, ListArgs, PathArgs, ShowArgs, TreeArgs};
pub(super) fn run_show(config: &TagsConfig, args: &ShowArgs, output: &OutputConfig) -> Result<()> {
let ontology = &config.ontology;
if ontology.is_empty() {
bail!("No ontology defined. Add [ontology.tags.*] sections to .bobbin/tags.toml");
}
let def = ontology
.tags
.get(&args.tag)
.ok_or_else(|| anyhow::anyhow!("Tag '{}' not found in ontology", args.tag))?;
if output.json {
let json = serde_json::json!({
"tag": args.tag,
"parent": def.parent,
"relates_to": def.relates_to,
"description": def.description,
"children": ontology.children(&args.tag),
"ancestors": config.tag_ancestors(&args.tag),
"related": config.related_tags(&args.tag),
});
println!("{}", serde_json::to_string_pretty(&json)?);
return Ok(());
}
println!("Tag: {}", args.tag);
if let Some(ref desc) = def.description {
println!(" Description: {}", desc);
}
if let Some(ref parent) = def.parent {
println!(" Parent: {}", parent);
}
let ancestors = config.tag_ancestors(&args.tag);
if !ancestors.is_empty() {
println!(
" Ancestry: {} → {}",
ancestors
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
.join(" → "),
args.tag
);
}
let children = ontology.children(&args.tag);
if !children.is_empty() {
println!(" Children: {}", children.join(", "));
}
if !def.relates_to.is_empty() {
println!(" Relates to: {}", def.relates_to.join(", "));
}
if let Some(effect) = config.effects.get(&args.tag) {
println!(
" Effect: boost={}, exclude={}, pin={}",
effect.boost, effect.exclude, effect.pin
);
}
let tagged_bundles: Vec<&str> = config
.bundles
.iter()
.filter(|b| b.tags.iter().any(|t| t == &args.tag))
.map(|b| b.name.as_str())
.collect();
if !tagged_bundles.is_empty() {
println!(" Bundles: {}", tagged_bundles.join(", "));
}
Ok(())
}
pub(super) fn run_expand(
config: &TagsConfig,
args: &ExpandArgs,
output: &OutputConfig,
) -> Result<()> {
let ontology = &config.ontology;
if ontology.is_empty() {
bail!("No ontology defined. Add [ontology.tags.*] sections to .bobbin/tags.toml");
}
if !ontology.tags.contains_key(&args.tag) {
bail!("Tag '{}' not found in ontology", args.tag);
}
let descendants = ontology.descendants(&args.tag);
if output.json {
let mut expanded = vec![args.tag.clone()];
expanded.extend(descendants.clone());
let json = serde_json::json!({
"tag": args.tag,
"descendants": descendants,
"expanded": expanded,
});
println!("{}", serde_json::to_string_pretty(&json)?);
return Ok(());
}
println!("Expanding: {}", args.tag);
if descendants.is_empty() {
println!(" (no descendants)");
} else {
print_tree_from(ontology, &args.tag, 1);
}
println!();
println!(
"Total: {} tag(s) (1 root + {} descendants)",
1 + descendants.len(),
descendants.len()
);
Ok(())
}
pub(super) fn run_path(config: &TagsConfig, args: &PathArgs, output: &OutputConfig) -> Result<()> {
let ontology = &config.ontology;
if ontology.is_empty() {
bail!("No ontology defined. Add [ontology.tags.*] sections to .bobbin/tags.toml");
}
let path = find_path(ontology, &args.from, &args.to);
if output.json {
let json = serde_json::json!({
"from": args.from,
"to": args.to,
"path": path,
"connected": !path.is_empty(),
});
println!("{}", serde_json::to_string_pretty(&json)?);
return Ok(());
}
if path.is_empty() {
println!("No path found from '{}' to '{}'", args.from, args.to);
} else {
println!("{}", path.join(" → "));
}
Ok(())
}
pub(super) fn run_list(config: &TagsConfig, args: &ListArgs, output: &OutputConfig) -> Result<()> {
let ontology = &config.ontology;
if ontology.is_empty() {
bail!("No ontology defined. Add [ontology.tags.*] sections to .bobbin/tags.toml");
}
let tags: Vec<&String> = if args.roots {
let roots = ontology.roots();
ontology
.tags
.keys()
.filter(|k| roots.contains(&k.to_string()))
.collect()
} else {
let mut keys: Vec<&String> = ontology.tags.keys().collect();
keys.sort();
keys
};
if output.json {
let entries: Vec<serde_json::Value> = tags
.iter()
.map(|name| {
let def = &ontology.tags[*name];
serde_json::json!({
"tag": name,
"parent": def.parent,
"relates_to": def.relates_to,
"description": def.description,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&entries)?);
return Ok(());
}
println!("Ontology: {} tag(s)", tags.len());
println!();
for name in &tags {
let def = &ontology.tags[*name];
let parent_info = def
.parent
.as_ref()
.map(|p| format!(" (parent: {})", p))
.unwrap_or_default();
let desc = def
.description
.as_ref()
.map(|d| format!(" — {}", d))
.unwrap_or_default();
println!(" {}{}{}", name, parent_info, desc);
}
Ok(())
}
pub(super) fn run_tree(config: &TagsConfig, args: &TreeArgs, output: &OutputConfig) -> Result<()> {
let ontology = &config.ontology;
if ontology.is_empty() {
bail!("No ontology defined. Add [ontology.tags.*] sections to .bobbin/tags.toml");
}
if output.json {
let tree = build_tree_json(ontology, args.root.as_deref());
println!("{}", serde_json::to_string_pretty(&tree)?);
return Ok(());
}
if let Some(ref root) = args.root {
if !ontology.tags.contains_key(root) {
bail!("Tag '{}' not found in ontology", root);
}
println!("{}", root);
print_tree_from(ontology, root, 1);
} else {
let mut roots = ontology.roots();
roots.sort();
for root in &roots {
let desc = ontology
.tags
.get(root)
.and_then(|d| d.description.as_ref())
.map(|d| format!(" — {}", d))
.unwrap_or_default();
println!("{}{}", root, desc);
print_tree_from(ontology, root, 1);
}
}
Ok(())
}
pub(super) fn print_tree_from(ontology: &OntologyConfig, tag: &str, depth: usize) {
let mut children = ontology.children(tag);
children.sort();
let indent = " ".repeat(depth);
for child in &children {
let desc = ontology
.tags
.get(child)
.and_then(|d| d.description.as_ref())
.map(|d| format!(" — {}", d))
.unwrap_or_default();
let relates = ontology
.tags
.get(child)
.map(|d| &d.relates_to)
.filter(|r| !r.is_empty())
.map(|r| format!(" [relates: {}]", r.join(", ")))
.unwrap_or_default();
println!("{}├─ {}{}{}", indent, child, desc, relates);
print_tree_from(ontology, child, depth + 1);
}
}
pub(super) fn find_path(ontology: &OntologyConfig, from: &str, to: &str) -> Vec<String> {
if from == to {
return vec![from.to_string()];
}
if !ontology.tags.contains_key(from) || !ontology.tags.contains_key(to) {
return vec![];
}
use std::collections::{HashMap, HashSet, VecDeque};
let mut visited: HashSet<String> = HashSet::new();
let mut prev: HashMap<String, String> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
visited.insert(from.to_string());
queue.push_back(from.to_string());
while let Some(current) = queue.pop_front() {
let mut neighbors: Vec<String> = Vec::new();
if let Some(def) = ontology.tags.get(¤t) {
if let Some(ref parent) = def.parent {
neighbors.push(parent.clone());
}
neighbors.extend(def.relates_to.iter().cloned());
}
for (name, def) in &ontology.tags {
if def.parent.as_deref() == Some(current.as_str()) {
neighbors.push(name.clone());
}
}
for neighbor in neighbors {
if !visited.contains(&neighbor) {
visited.insert(neighbor.clone());
prev.insert(neighbor.clone(), current.clone());
if neighbor == to {
let mut path = vec![to.to_string()];
let mut c = to.to_string();
while let Some(p) = prev.get(&c) {
path.push(p.clone());
c = p.clone();
}
path.reverse();
return path;
}
queue.push_back(neighbor);
}
}
}
vec![] }
pub(super) fn build_tree_json(ontology: &OntologyConfig, root: Option<&str>) -> serde_json::Value {
fn node_json(ontology: &OntologyConfig, tag: &str) -> serde_json::Value {
let def = ontology.tags.get(tag);
let mut children: Vec<String> = ontology.children(tag);
children.sort();
let children_json: Vec<serde_json::Value> =
children.iter().map(|c| node_json(ontology, c)).collect();
serde_json::json!({
"tag": tag,
"description": def.and_then(|d| d.description.as_ref()),
"relates_to": def.map(|d| &d.relates_to).unwrap_or(&vec![]),
"children": children_json,
})
}
if let Some(root) = root {
node_json(ontology, root)
} else {
let mut roots = ontology.roots();
roots.sort();
let trees: Vec<serde_json::Value> = roots.iter().map(|r| node_json(ontology, r)).collect();
serde_json::json!(trees)
}
}