use crate::{Axis, Cluster, ClusterId, ClusterIdSegment, Diagnostics, FaceError, Record, Strategy};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AxisPlan {
pub axis: Axis,
pub within: Vec<AxisPlan>,
}
impl AxisPlan {
pub fn leaf(axis: Axis) -> Self {
Self {
axis,
within: Vec::new(),
}
}
pub fn with(axis: Axis, within: AxisPlan) -> Self {
Self {
axis,
within: vec![within],
}
}
pub fn with_many(axis: Axis, within: Vec<AxisPlan>) -> Self {
Self { axis, within }
}
}
pub fn build_tree<D: Diagnostics + ?Sized>(
plan: &AxisPlan,
items: Vec<Record>,
diag: &mut D,
) -> Result<Vec<Cluster>, FaceError> {
build_tree_impl(plan, items, &ClusterId::new(Vec::new()), diag)
}
pub(crate) fn build_tree_impl<D: Diagnostics + ?Sized>(
plan: &AxisPlan,
items: Vec<Record>,
parent: &ClusterId,
diag: &mut D,
) -> Result<Vec<Cluster>, FaceError> {
match &plan.axis.strategy {
Strategy::Exact => exact::cluster(plan, items, parent, diag),
Strategy::Prefix { .. } => prefix::cluster(plan, items, parent, diag),
Strategy::Top { .. } => top::cluster(plan, items, parent, diag),
Strategy::Bands { .. } => bands::cluster(plan, items, parent, diag),
Strategy::Quantiles { .. } => quantiles::cluster(plan, items, parent, diag),
Strategy::Natural { .. } => natural::cluster(plan, items, parent, diag),
Strategy::Similar { .. } => similar::cluster(plan, items, parent, diag),
#[expect(
unreachable_patterns,
reason = "guard for `#[non_exhaustive]` variants added in future slices"
)]
other => Err(FaceError::Unsupported {
feature: format!("strategy `{}` unsupported by this build", other.name()),
}),
}
}
pub(crate) fn extend_id(parent: &ClusterId, axis: &str, value: &str) -> ClusterId {
let mut segs = parent.segments().to_vec();
segs.push(ClusterIdSegment {
axis: axis.to_string(),
value: value.to_string(),
});
ClusterId::new(segs)
}
mod bands;
mod exact;
mod natural;
mod prefix;
mod quantiles;
mod similar;
mod top;
mod util;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use crate::{NullDiagnostics, VecDiagnostics};
fn axis(field: &str, strategy: Strategy) -> Axis {
Axis {
field: field.into(),
strategy,
auto: false,
}
}
fn records(items: Vec<serde_json::Value>) -> Vec<Record> {
Record::from_items(items, None)
}
#[test]
fn exact_single_axis_groups_by_value() {
let plan = AxisPlan::leaf(axis("kind", Strategy::Exact));
let items = vec![
json!({"kind": "alpha"}),
json!({"kind": "beta"}),
json!({"kind": "alpha"}),
json!({"kind": "alpha"}),
json!({"kind": "beta"}),
];
let mut diag = NullDiagnostics;
let clusters = build_tree(&plan, records(items), &mut diag).unwrap();
assert_eq!(clusters.len(), 2);
assert_eq!(clusters[0].label, "alpha");
assert_eq!(clusters[0].total, 3);
assert_eq!(clusters[1].label, "beta");
assert_eq!(clusters[1].total, 2);
assert!(clusters[0].clusters.is_empty());
assert!(clusters[1].clusters.is_empty());
}
#[test]
fn exact_two_axis_via_within() {
let plan = AxisPlan::with(
axis("repo", Strategy::Exact),
AxisPlan::leaf(axis("kind", Strategy::Exact)),
);
let items = vec![
json!({"repo": "r1", "kind": "a"}),
json!({"repo": "r1", "kind": "a"}),
json!({"repo": "r1", "kind": "b"}),
json!({"repo": "r2", "kind": "a"}),
];
let mut diag = VecDiagnostics::default();
let clusters = build_tree(&plan, records(items), &mut diag).unwrap();
assert_eq!(clusters.len(), 2);
assert_eq!(clusters[0].label, "r1");
assert_eq!(clusters[0].total, 3);
assert_eq!(clusters[0].clusters.len(), 2);
assert_eq!(clusters[0].clusters[0].label, "a");
assert_eq!(clusters[0].clusters[0].total, 2);
assert_eq!(clusters[0].clusters[1].label, "b");
assert_eq!(clusters[0].clusters[1].total, 1);
assert_eq!(clusters[1].label, "r2");
assert_eq!(clusters[1].total, 1);
assert_eq!(clusters[1].clusters.len(), 1);
assert_eq!(clusters[1].clusters[0].label, "a");
let child_id = &clusters[0].clusters[0].id;
assert_eq!(child_id.depth(), 2);
assert_eq!(child_id.segments()[0].axis, "repo");
assert_eq!(child_id.segments()[0].value, "r1");
assert_eq!(child_id.segments()[1].axis, "kind");
assert_eq!(child_id.segments()[1].value, "a");
}
#[test]
fn bands_dispatches_to_numeric_clusterer() {
let plan = AxisPlan::leaf(axis("score", Strategy::Bands { count: 5 }));
let items = vec![json!({"score": 0.9})];
let mut diag = NullDiagnostics;
let clusters = build_tree(&plan, records(items), &mut diag).unwrap();
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].total, 1);
}
#[test]
fn exact_within_bands_recurses_into_numeric_axis() {
let plan = AxisPlan::with(
axis("kind", Strategy::Exact),
AxisPlan::leaf(axis("score", Strategy::Bands { count: 5 })),
);
let items = vec![json!({"kind": "a", "score": 0.9})];
let mut diag = NullDiagnostics;
let clusters = build_tree(&plan, records(items), &mut diag).unwrap();
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].label, "a");
assert_eq!(clusters[0].clusters.len(), 1);
assert_eq!(clusters[0].clusters[0].total, 1);
}
}