use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct Cluster {
pub id: String,
pub title: String,
pub spec_section: String,
#[serde(default)]
pub note: Option<String>,
}
pub fn validate_cluster_refs<'a>(
clusters: &[Cluster],
refs: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Result<(), String> {
let known: std::collections::HashSet<&str> = clusters.iter().map(|c| c.id.as_str()).collect();
for (input, cluster_id) in refs {
if !known.contains(cluster_id) {
return Err(format!(
"input {input:?} references unknown cluster id {cluster_id:?}; \
every `cluster` ref must resolve to a `clusters` registry entry"
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn cluster(id: &str) -> Cluster {
Cluster {
id: id.to_string(),
title: format!("title for {id}"),
spec_section: "recommendations/DNA/example.md".to_string(),
note: None,
}
}
#[test]
fn dangling_cluster_ref_is_rejected() {
let clusters = vec![cluster("refseqgene-selector")];
let refs = vec![
("NG_1(NM_1):c.1del", "refseqgene-selector"),
("NG_2(NM_2):c.2del", "typo-cluster"),
];
let err = validate_cluster_refs(&clusters, refs)
.expect_err("a reference to an unknown cluster id must be rejected");
assert!(
err.contains("typo-cluster"),
"error should name the dangling id: {err}"
);
assert!(
err.contains("NG_2(NM_2):c.2del"),
"error should name the offending input for context: {err}"
);
}
#[test]
fn orphan_cluster_is_allowed() {
let clusters = vec![cluster("used"), cluster("never-referenced")];
let refs = vec![("NM_1:c.1del", "used")];
assert!(
validate_cluster_refs(&clusters, refs).is_ok(),
"an unreferenced cluster is durable taxonomy, not an error"
);
}
#[test]
fn no_references_is_ok() {
let clusters: Vec<Cluster> = vec![];
let refs: Vec<(&str, &str)> = vec![];
assert!(validate_cluster_refs(&clusters, refs).is_ok());
}
}