use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{ClusterId, FaceError};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct Cluster {
pub id: ClusterId,
pub label: String,
pub axis: String,
pub value: serde_json::Value,
pub total: u64,
pub score_min: Option<f64>,
pub score_max: Option<f64>,
pub clusters: Vec<Cluster>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct FlatCluster {
pub id: ClusterId,
pub parent_id: Option<ClusterId>,
pub label: String,
pub axis: String,
pub value: serde_json::Value,
pub total: u64,
pub score_min: Option<f64>,
pub score_max: Option<f64>,
}
pub fn to_flat(roots: &[Cluster]) -> Vec<FlatCluster> {
let mut out = Vec::new();
for root in roots {
flatten_into(root, None, &mut out);
}
out
}
fn flatten_into(node: &Cluster, parent_id: Option<&ClusterId>, out: &mut Vec<FlatCluster>) {
out.push(FlatCluster {
id: node.id.clone(),
parent_id: parent_id.cloned(),
label: node.label.clone(),
axis: node.axis.clone(),
value: node.value.clone(),
total: node.total,
score_min: node.score_min,
score_max: node.score_max,
});
for child in &node.clusters {
flatten_into(child, Some(&node.id), out);
}
}
pub fn from_flat(flat: &[FlatCluster]) -> Result<Vec<Cluster>, FaceError> {
let id_set: std::collections::HashSet<&ClusterId> = flat.iter().map(|c| &c.id).collect();
for c in flat {
if let Some(p) = &c.parent_id
&& !id_set.contains(p)
{
return Err(FaceError::InvalidClusterId {
segment: 0,
reason: "parent_id references unknown id".to_string(),
});
}
}
let mut children_of: HashMap<Option<ClusterId>, Vec<usize>> = HashMap::new();
for (i, c) in flat.iter().enumerate() {
children_of.entry(c.parent_id.clone()).or_default().push(i);
}
fn build(
idx: usize,
flat: &[FlatCluster],
children_of: &HashMap<Option<ClusterId>, Vec<usize>>,
) -> Cluster {
let f = &flat[idx];
let kids = children_of
.get(&Some(f.id.clone()))
.map(|v| v.iter().map(|&i| build(i, flat, children_of)).collect())
.unwrap_or_default();
Cluster {
id: f.id.clone(),
label: f.label.clone(),
axis: f.axis.clone(),
value: f.value.clone(),
total: f.total,
score_min: f.score_min,
score_max: f.score_max,
clusters: kids,
}
}
let roots = children_of
.get(&None)
.map(|v| v.iter().map(|&i| build(i, flat, &children_of)).collect())
.unwrap_or_default();
Ok(roots)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ClusterIdSegment;
use serde_json::Value;
fn seg(axis: &str, value: &str) -> ClusterIdSegment {
ClusterIdSegment {
axis: axis.into(),
value: value.into(),
}
}
fn leaf(id: ClusterId, axis: &str, value: &str, total: u64) -> Cluster {
Cluster {
id,
label: value.to_string(),
axis: axis.to_string(),
value: Value::String(value.to_string()),
total,
score_min: None,
score_max: None,
clusters: vec![],
}
}
#[test]
fn one_deep_tree_round_trips() {
let parent_id = ClusterId::new(vec![seg("file", "src/cli.rs")]);
let child_id = ClusterId::new(vec![seg("file", "src/cli.rs"), seg("score", "excellent")]);
let child = leaf(child_id, "score", "excellent", 12);
let parent = Cluster {
clusters: vec![child],
..leaf(parent_id, "file", "src/cli.rs", 38)
};
let flat = to_flat(std::slice::from_ref(&parent));
assert_eq!(flat.len(), 2);
assert!(flat[0].parent_id.is_none());
assert_eq!(flat[1].parent_id.as_ref(), Some(&parent.id));
let nested = from_flat(&flat).unwrap();
assert_eq!(nested, vec![parent]);
}
#[test]
fn dangling_parent_id_is_an_error() {
let id = ClusterId::new(vec![seg("score", "excellent")]);
let dangling_parent = ClusterId::new(vec![seg("file", "missing")]);
let flat = vec![FlatCluster {
id,
parent_id: Some(dangling_parent),
label: "excellent".into(),
axis: "score".into(),
value: Value::String("excellent".into()),
total: 1,
score_min: None,
score_max: None,
}];
let err = from_flat(&flat).unwrap_err();
assert!(matches!(
err,
FaceError::InvalidClusterId { segment: 0, .. }
));
}
}