use serde::Serialize;
use crate::graph::KnowledgeGraph;
use crate::model::{FieldName, ItemType};
#[derive(Debug, Clone, Serialize)]
pub struct MatrixRow {
pub source_id: String,
pub source_name: String,
pub source_type: String,
pub targets: Vec<MatrixTarget>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MatrixTarget {
pub id: String,
pub name: String,
pub target_type: String,
pub relationship: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct TraceabilityMatrix {
pub rows: Vec<MatrixRow>,
pub columns: Vec<String>,
pub total_relationships: usize,
}
impl TraceabilityMatrix {
pub fn generate(graph: &KnowledgeGraph) -> Self {
let mut rows: Vec<MatrixRow> = graph
.items()
.map(|item| Self::build_row(item, graph))
.collect();
let total_relationships = rows.iter().map(|r| r.targets.len()).sum();
Self::sort_rows(&mut rows);
let columns = Self::build_columns();
Self {
rows,
columns,
total_relationships,
}
}
fn build_row(item: &crate::model::Item, graph: &KnowledgeGraph) -> MatrixRow {
let mut targets = Vec::new();
Self::collect_upstream_targets(item, graph, &mut targets);
Self::collect_downstream_targets(item, graph, &mut targets);
MatrixRow {
source_id: item.id.as_str().to_string(),
source_name: item.name.clone(),
source_type: item.item_type.display_name().to_string(),
targets,
}
}
fn collect_upstream_targets(
item: &crate::model::Item,
graph: &KnowledgeGraph,
targets: &mut Vec<MatrixTarget>,
) {
Self::add_targets(
&item.upstream.refines,
FieldName::Refines.as_str(),
graph,
targets,
);
Self::add_targets(
&item.upstream.derives_from,
FieldName::DerivesFrom.as_str(),
graph,
targets,
);
Self::add_targets(
&item.upstream.satisfies,
FieldName::Satisfies.as_str(),
graph,
targets,
);
}
fn collect_downstream_targets(
item: &crate::model::Item,
graph: &KnowledgeGraph,
targets: &mut Vec<MatrixTarget>,
) {
Self::add_targets(
&item.downstream.is_refined_by,
FieldName::IsRefinedBy.as_str(),
graph,
targets,
);
Self::add_targets(
&item.downstream.derives,
FieldName::Derives.as_str(),
graph,
targets,
);
Self::add_targets(
&item.downstream.is_satisfied_by,
FieldName::IsSatisfiedBy.as_str(),
graph,
targets,
);
}
fn add_targets(
ref_ids: &[crate::model::ItemId],
relationship: &str,
graph: &KnowledgeGraph,
targets: &mut Vec<MatrixTarget>,
) {
for ref_id in ref_ids {
if let Some(target) = graph.get(ref_id) {
targets.push(MatrixTarget {
id: ref_id.as_str().to_string(),
name: target.name.clone(),
target_type: target.item_type.display_name().to_string(),
relationship: relationship.to_string(),
});
}
}
}
fn sort_rows(rows: &mut [MatrixRow]) {
rows.sort_by(|a, b| {
let type_order_a = Self::type_order(&a.source_type);
let type_order_b = Self::type_order(&b.source_type);
type_order_a
.cmp(&type_order_b)
.then(a.source_id.cmp(&b.source_id))
});
}
fn build_columns() -> Vec<String> {
ItemType::all()
.iter()
.map(|t| t.display_name().to_string())
.collect()
}
fn type_order(type_name: &str) -> usize {
match type_name {
"Solution" => 0,
"Use Case" => 1,
"Scenario" => 2,
"System Requirement" => 3,
"System Architecture" => 4,
"Hardware Requirement" => 5,
"Software Requirement" => 6,
"Hardware Detailed Design" => 7,
"Software Detailed Design" => 8,
_ => 9,
}
}
pub fn to_csv(&self) -> String {
let mut csv = String::new();
csv.push_str(
"Source ID,Source Name,Source Type,Target ID,Target Name,Target Type,Relationship\n",
);
for row in &self.rows {
if row.targets.is_empty() {
csv.push_str(&format!(
"{},{},{},,,, \n",
Self::escape_csv(&row.source_id),
Self::escape_csv(&row.source_name),
Self::escape_csv(&row.source_type),
));
} else {
for target in &row.targets {
csv.push_str(&format!(
"{},{},{},{},{},{},{}\n",
Self::escape_csv(&row.source_id),
Self::escape_csv(&row.source_name),
Self::escape_csv(&row.source_type),
Self::escape_csv(&target.id),
Self::escape_csv(&target.name),
Self::escape_csv(&target.target_type),
Self::escape_csv(&target.relationship),
));
}
}
}
csv
}
fn escape_csv(value: &str) -> String {
if value.contains(',') || value.contains('"') || value.contains('\n') {
format!("\"{}\"", value.replace('"', "\"\""))
} else {
value.to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::KnowledgeGraphBuilder;
use crate::model::{ItemId, UpstreamRefs};
use crate::test_utils::{create_test_item, create_test_item_with_upstream};
#[test]
fn test_matrix_generation() {
let sol = create_test_item("SOL-001", ItemType::Solution);
let uc = create_test_item_with_upstream(
"UC-001",
ItemType::UseCase,
UpstreamRefs {
refines: vec![ItemId::new_unchecked("SOL-001")],
..Default::default()
},
);
let graph = KnowledgeGraphBuilder::new()
.add_item(sol)
.add_item(uc)
.build()
.unwrap();
let matrix = TraceabilityMatrix::generate(&graph);
assert_eq!(matrix.rows.len(), 2);
assert!(matrix.total_relationships > 0);
}
#[test]
fn test_matrix_csv() {
let sol = create_test_item("SOL-001", ItemType::Solution);
let graph = KnowledgeGraphBuilder::new().add_item(sol).build().unwrap();
let matrix = TraceabilityMatrix::generate(&graph);
let csv = matrix.to_csv();
assert!(csv.contains("Source ID"));
assert!(csv.contains("SOL-001"));
}
}