1use std::collections::HashMap;
12
13use serde::{Deserialize, Serialize};
14
15use crate::{ClusterId, FaceError};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22#[non_exhaustive]
23pub struct Cluster {
24 pub id: ClusterId,
26 pub label: String,
28 pub axis: String,
30 pub value: serde_json::Value,
32 pub total: u64,
34 pub score_min: Option<f64>,
36 pub score_max: Option<f64>,
38 pub clusters: Vec<Cluster>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[non_exhaustive]
48pub struct FlatCluster {
49 pub id: ClusterId,
51 pub parent_id: Option<ClusterId>,
53 pub label: String,
55 pub axis: String,
57 pub value: serde_json::Value,
59 pub total: u64,
61 pub score_min: Option<f64>,
63 pub score_max: Option<f64>,
65}
66
67pub fn to_flat(roots: &[Cluster]) -> Vec<FlatCluster> {
94 let mut out = Vec::new();
95 for root in roots {
96 flatten_into(root, None, &mut out);
97 }
98 out
99}
100
101fn flatten_into(node: &Cluster, parent_id: Option<&ClusterId>, out: &mut Vec<FlatCluster>) {
102 out.push(FlatCluster {
103 id: node.id.clone(),
104 parent_id: parent_id.cloned(),
105 label: node.label.clone(),
106 axis: node.axis.clone(),
107 value: node.value.clone(),
108 total: node.total,
109 score_min: node.score_min,
110 score_max: node.score_max,
111 });
112 for child in &node.clusters {
113 flatten_into(child, Some(&node.id), out);
114 }
115}
116
117pub fn from_flat(flat: &[FlatCluster]) -> Result<Vec<Cluster>, FaceError> {
124 let id_set: std::collections::HashSet<&ClusterId> = flat.iter().map(|c| &c.id).collect();
127 for c in flat {
128 if let Some(p) = &c.parent_id
129 && !id_set.contains(p)
130 {
131 return Err(FaceError::InvalidClusterId {
132 segment: 0,
133 reason: "parent_id references unknown id".to_string(),
134 });
135 }
136 }
137
138 let mut children_of: HashMap<Option<ClusterId>, Vec<usize>> = HashMap::new();
140 for (i, c) in flat.iter().enumerate() {
141 children_of.entry(c.parent_id.clone()).or_default().push(i);
142 }
143
144 fn build(
145 idx: usize,
146 flat: &[FlatCluster],
147 children_of: &HashMap<Option<ClusterId>, Vec<usize>>,
148 ) -> Cluster {
149 let f = &flat[idx];
150 let kids = children_of
151 .get(&Some(f.id.clone()))
152 .map(|v| v.iter().map(|&i| build(i, flat, children_of)).collect())
153 .unwrap_or_default();
154 Cluster {
155 id: f.id.clone(),
156 label: f.label.clone(),
157 axis: f.axis.clone(),
158 value: f.value.clone(),
159 total: f.total,
160 score_min: f.score_min,
161 score_max: f.score_max,
162 clusters: kids,
163 }
164 }
165
166 let roots = children_of
167 .get(&None)
168 .map(|v| v.iter().map(|&i| build(i, flat, &children_of)).collect())
169 .unwrap_or_default();
170 Ok(roots)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::ClusterIdSegment;
177 use serde_json::Value;
178
179 fn seg(axis: &str, value: &str) -> ClusterIdSegment {
180 ClusterIdSegment {
181 axis: axis.into(),
182 value: value.into(),
183 }
184 }
185
186 fn leaf(id: ClusterId, axis: &str, value: &str, total: u64) -> Cluster {
187 Cluster {
188 id,
189 label: value.to_string(),
190 axis: axis.to_string(),
191 value: Value::String(value.to_string()),
192 total,
193 score_min: None,
194 score_max: None,
195 clusters: vec![],
196 }
197 }
198
199 #[test]
200 fn one_deep_tree_round_trips() {
201 let parent_id = ClusterId::new(vec![seg("file", "src/cli.rs")]);
202 let child_id = ClusterId::new(vec![seg("file", "src/cli.rs"), seg("score", "excellent")]);
203
204 let child = leaf(child_id, "score", "excellent", 12);
205 let parent = Cluster {
206 clusters: vec![child],
207 ..leaf(parent_id, "file", "src/cli.rs", 38)
208 };
209
210 let flat = to_flat(std::slice::from_ref(&parent));
211 assert_eq!(flat.len(), 2);
212 assert!(flat[0].parent_id.is_none());
213 assert_eq!(flat[1].parent_id.as_ref(), Some(&parent.id));
214
215 let nested = from_flat(&flat).unwrap();
216 assert_eq!(nested, vec![parent]);
217 }
218
219 #[test]
220 fn dangling_parent_id_is_an_error() {
221 let id = ClusterId::new(vec![seg("score", "excellent")]);
222 let dangling_parent = ClusterId::new(vec![seg("file", "missing")]);
223 let flat = vec![FlatCluster {
224 id,
225 parent_id: Some(dangling_parent),
226 label: "excellent".into(),
227 axis: "score".into(),
228 value: Value::String("excellent".into()),
229 total: 1,
230 score_min: None,
231 score_max: None,
232 }];
233 let err = from_flat(&flat).unwrap_err();
234 assert!(matches!(
235 err,
236 FaceError::InvalidClusterId { segment: 0, .. }
237 ));
238 }
239}