use std::error::Error;
use std::process::ExitCode;
use clap::Args;
use sara_core::graph::{
KnowledgeGraph, KnowledgeGraphBuilder, LookupResult, TraversalNode, TraversalOptions,
TraversalResult, traverse_downstream, traverse_upstream,
};
use sara_core::model::{Item, ItemId, ItemType};
use sara_core::config::{Config, OutputConfig};
use crate::output::{
Color, EMOJI_ERROR, EMOJI_ITEM, Style, colorize, format_tree_branch, get_emoji, print_header,
};
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum QueryFormat {
#[default]
Tree,
Json,
}
#[derive(Args, Debug)]
pub struct QueryArgs {
pub item_id: String,
#[arg(long, help_heading = "Filters")]
pub depth: Option<usize>,
#[arg(short = 't', long = "type", help_heading = "Filters")]
pub item_types: Vec<String>,
#[arg(short, long, help_heading = "Traversal")]
pub downstream: bool,
#[arg(short, long, help_heading = "Traversal")]
pub upstream: bool,
#[arg(long, default_value = "tree", help_heading = "Output")]
pub format: QueryFormat,
}
pub fn run(args: &QueryArgs, config: &Config) -> Result<ExitCode, Box<dyn Error>> {
let items = super::parse_items(config)?;
let graph = KnowledgeGraphBuilder::new().add_items(items).build()?;
match graph.lookup(&args.item_id) {
LookupResult::Found(item) => handle_found_item(args, &config.output, item, &graph),
LookupResult::NotFound { suggestions } => {
handle_not_found(args, &config.output, &suggestions)
}
}
}
fn handle_found_item(
args: &QueryArgs,
config: &OutputConfig,
item: &Item,
graph: &KnowledgeGraph,
) -> Result<ExitCode, Box<dyn Error>> {
print_item_info(config, item, graph);
if args.upstream || args.downstream {
print_traceability(args, config, item, graph);
} else {
print_direct_relationships(config, item, graph);
}
Ok(ExitCode::SUCCESS)
}
fn print_traceability(
args: &QueryArgs,
config: &OutputConfig,
item: &Item,
graph: &KnowledgeGraph,
) {
let traversal_opts = build_traversal_options(args);
if args.upstream {
println!();
print_header(config, &format!("Upstream Traceability for {}", item.id));
if let Some(result) = traverse_upstream(graph, &item.id, &traversal_opts) {
print_traversal(config, &result, graph, args);
}
}
if args.downstream {
println!();
print_header(config, &format!("Downstream from {}", item.id));
if let Some(result) = traverse_downstream(graph, &item.id, &traversal_opts) {
print_traversal(config, &result, graph, args);
}
}
}
fn handle_not_found(
args: &QueryArgs,
config: &OutputConfig,
suggestions: &[&ItemId],
) -> Result<ExitCode, Box<dyn Error>> {
let emoji = get_emoji(config, &EMOJI_ERROR);
let id = colorize(config, &args.item_id, Color::Red, Style::None);
println!("{} Item not found: {}", emoji, id);
if !suggestions.is_empty() {
println!();
println!("Did you mean?");
for suggestion in suggestions {
println!(" • {}", suggestion.as_str());
}
}
Ok(ExitCode::from(1))
}
fn build_traversal_options(args: &QueryArgs) -> TraversalOptions {
let mut traversal_opts = TraversalOptions::new();
if let Some(depth) = args.depth {
traversal_opts = traversal_opts.with_max_depth(depth);
}
let types = parse_item_types(&args.item_types);
if !types.is_empty() {
traversal_opts = traversal_opts.with_types(types);
}
traversal_opts
}
fn print_item_info(config: &OutputConfig, item: &Item, _graph: &KnowledgeGraph) {
let emoji = get_emoji(config, &EMOJI_ITEM);
let id = colorize(config, item.id.as_str(), Color::Cyan, Style::Bold);
let item_type = colorize(
config,
item.item_type.display_name(),
Color::None,
Style::Dimmed,
);
let desc = item
.description
.as_ref()
.map(|d| format!("\n Description: {d}"))
.unwrap_or_default();
println!(
"{emoji} {id}: {name}
Type: {item_type}
File: {file}{desc}",
name = item.name,
file = item.source.file_path.display(),
);
}
fn print_direct_relationships(config: &OutputConfig, item: &Item, graph: &KnowledgeGraph) {
let parents = graph.parents(&item.id);
if !parents.is_empty() {
let label = colorize(config, "Requires:", Color::None, Style::Bold);
println!("\n {label}");
for (i, parent) in parents.iter().enumerate() {
let branch = format_tree_branch(i == parents.len() - 1);
let id = colorize(config, parent.id.as_str(), Color::Cyan, Style::None);
println!(" {branch} {id}: {name}", name = parent.name);
}
}
let children = graph.children(&item.id);
if !children.is_empty() {
let label = colorize(config, "Realized by:", Color::None, Style::Bold);
println!("\n {label}");
for (i, child) in children.iter().enumerate() {
let branch = format_tree_branch(i == children.len() - 1);
let id = colorize(config, child.id.as_str(), Color::Cyan, Style::None);
println!(" {branch} {id}: {name}", name = child.name);
}
}
}
fn print_traversal(
config: &OutputConfig,
result: &TraversalResult,
graph: &KnowledgeGraph,
args: &QueryArgs,
) {
match args.format {
QueryFormat::Tree => print_traversal_tree(config, result, graph),
QueryFormat::Json => print_traversal_json(result, graph),
}
}
fn print_traversal_tree(config: &OutputConfig, result: &TraversalResult, graph: &KnowledgeGraph) {
let mut children_map: std::collections::HashMap<Option<&ItemId>, Vec<&TraversalNode>> =
std::collections::HashMap::new();
for node in &result.items {
children_map
.entry(node.parent.as_ref())
.or_default()
.push(node);
}
if let Some(roots) = children_map.get(&None) {
for (i, root) in roots.iter().enumerate() {
let is_last = i == roots.len() - 1;
print_tree_node(config, root, graph, &children_map, "", is_last, true);
}
}
}
fn print_tree_node(
config: &OutputConfig,
node: &TraversalNode,
graph: &KnowledgeGraph,
children_map: &std::collections::HashMap<Option<&ItemId>, Vec<&TraversalNode>>,
prefix: &str,
is_last: bool,
is_root: bool,
) {
let item = match graph.get(&node.item_id) {
Some(item) => item,
None => return,
};
let branch = if is_root {
""
} else if is_last {
"└── "
} else {
"├── "
};
let id = colorize(config, item.id.as_str(), Color::Cyan, Style::None);
let type_name = colorize(
config,
item.item_type.display_name(),
Color::None,
Style::Dimmed,
);
let item_text = format!("{}: {} ({})", id, item.name, type_name);
if is_root {
println!("{}", item_text);
} else {
println!("{}{}{}", prefix, branch, item_text);
}
if let Some(children) = children_map.get(&Some(&node.item_id)) {
let new_prefix = if is_root {
String::new()
} else {
format!("{}{}", prefix, if is_last { " " } else { "│ " })
};
for (i, child) in children.iter().enumerate() {
let child_is_last = i == children.len() - 1;
print_tree_node(
config,
child,
graph,
children_map,
&new_prefix,
child_is_last,
false,
);
}
}
}
fn print_traversal_json(result: &TraversalResult, graph: &KnowledgeGraph) {
#[derive(serde::Serialize)]
struct JsonNode {
id: String,
name: String,
item_type: String,
depth: usize,
parent: Option<String>,
}
let nodes: Vec<JsonNode> = result
.items
.iter()
.filter_map(|node| {
graph.get(&node.item_id).map(|item| JsonNode {
id: item.id.as_str().to_string(),
name: item.name.clone(),
item_type: item.item_type.display_name().to_string(),
depth: node.depth,
parent: node.parent.as_ref().map(|p| p.as_str().to_string()),
})
})
.collect();
let json_output = serde_json::json!({
"origin": result.origin.as_str(),
"max_depth": result.max_depth,
"items": nodes
});
println!("{}", serde_json::to_string_pretty(&json_output).unwrap());
}
pub fn parse_item_types(types: &[String]) -> Vec<ItemType> {
types
.iter()
.filter_map(|t| match t.to_lowercase().as_str() {
"solution" => Some(ItemType::Solution),
"use_case" | "usecase" => Some(ItemType::UseCase),
"scenario" => Some(ItemType::Scenario),
"system_requirement" | "systemrequirement" => Some(ItemType::SystemRequirement),
"system_architecture" | "systemarchitecture" => Some(ItemType::SystemArchitecture),
"hardware_requirement" | "hardwarerequirement" => Some(ItemType::HardwareRequirement),
"software_requirement" | "softwarerequirement" => Some(ItemType::SoftwareRequirement),
"hardware_detailed_design" | "hardwaredetaileddesign" => {
Some(ItemType::HardwareDetailedDesign)
}
"software_detailed_design" | "softwaredetaileddesign" => {
Some(ItemType::SoftwareDetailedDesign)
}
_ => None,
})
.collect()
}