use serde::Deserialize;
use super::schema::{validate_cluster_refs, Cluster};
use super::summary::{DispositionKind, MemberRow, SummaryModel};
pub const CORPUS_TITLE: &str = "biocommons-normalize";
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct Fixture {
pub description: String,
pub source: String,
pub source_commit: String,
pub biocommons_upstream: String,
pub license: String,
pub refreshed_at: String,
#[serde(default)]
pub clusters: Vec<Cluster>,
pub cases: Vec<Case>,
}
impl Fixture {
pub fn cluster_refs(&self) -> Vec<(&str, &str)> {
self.cases
.iter()
.filter_map(|case| {
let cluster = case.disposition.as_ref()?.cluster()?;
Some((case.input.as_str(), cluster))
})
.collect()
}
pub fn validate_clusters(&self) -> Result<(), String> {
validate_cluster_refs(&self.clusters, self.cluster_refs())
}
pub fn to_summary(&self) -> SummaryModel {
let rows = self
.cases
.iter()
.filter_map(|case| {
let d = case.disposition.as_ref()?;
let (kind, ferro_output, tracking_issue, cluster) = match d {
Disposition::AcceptedDivergence {
ferro_output,
cluster,
..
} => (
DispositionKind::AcceptedDivergence,
Some(ferro_output.clone()),
None,
cluster.clone(),
),
Disposition::KnownBug {
ferro_output,
tracking_issue,
cluster,
} => (
DispositionKind::KnownBug,
Some(ferro_output.clone()),
Some(*tracking_issue),
cluster.clone(),
),
Disposition::Improvement {
ferro_output,
tracking_issue,
cluster,
..
} => (
DispositionKind::Improvement,
Some(ferro_output.clone()),
Some(*tracking_issue),
cluster.clone(),
),
};
Some(MemberRow {
cluster,
input: case.input.clone(),
axis: "normalized".to_string(),
kind,
ferro_output,
tracking_issue,
})
})
.collect();
SummaryModel {
title: CORPUS_TITLE.to_string(),
clusters: self.clusters.clone(),
rows,
}
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct Case {
pub input: String,
pub normalized: Option<String>,
#[serde(default)]
pub expects_error: bool,
pub shuffle_direction: String,
pub cross_boundaries: bool,
pub source_function: String,
#[serde(default = "default_true")]
pub to_test: bool,
#[serde(default)]
pub disposition: Option<Disposition>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[allow(dead_code)] pub enum Disposition {
AcceptedDivergence {
reason: String,
#[serde(default)]
spec_citation: Option<String>,
ferro_output: String,
#[serde(default)]
cluster: Option<String>,
},
KnownBug {
tracking_issue: u64,
ferro_output: String,
#[serde(default)]
cluster: Option<String>,
},
Improvement {
tracking_issue: u64,
section: String,
ferro_output: String,
#[serde(default)]
cluster: Option<String>,
},
}
impl Disposition {
pub fn cluster(&self) -> Option<&str> {
match self {
Disposition::AcceptedDivergence { cluster, .. }
| Disposition::KnownBug { cluster, .. }
| Disposition::Improvement { cluster, .. } => cluster.as_deref(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(clusters: &str, disposition: &str) -> Fixture {
let json = format!(
r#"{{"description":"t","source":"t","source_commit":"t",
"biocommons_upstream":"t","license":"t","refreshed_at":"t",
"clusters":[{clusters}],
"cases":[{{"input":"X","normalized":"Y","shuffle_direction":"3prime",
"cross_boundaries":false,"source_function":"t","disposition":{disposition}}}]}}"#
);
serde_json::from_str(&json).expect("fixture should deserialize")
}
#[test]
fn cluster_ref_resolves_to_registry() {
let clusters = r#"{"id":"panic","title":"overflow panic","spec_section":"x"}"#;
let disp =
r#"{"kind":"known_bug","tracking_issue":472,"ferro_output":"Z","cluster":"panic"}"#;
let fixture = parse(clusters, disp);
assert_eq!(fixture.cluster_refs(), vec![("X", "panic")]);
assert!(fixture.validate_clusters().is_ok());
let summary = fixture.to_summary();
assert_eq!(summary.title, "biocommons-normalize");
assert_eq!(summary.rows.len(), 1);
let row = &summary.rows[0];
assert_eq!(row.kind, DispositionKind::KnownBug);
assert_eq!(row.axis, "normalized");
assert_eq!(row.ferro_output.as_deref(), Some("Z"));
assert_eq!(row.tracking_issue, Some(472));
assert_eq!(row.cluster.as_deref(), Some("panic"));
}
#[test]
fn improvement_cluster_ref_and_summary_are_preserved() {
let clusters = r#"{"id":"pref","title":"preferred form","spec_section":"x"}"#;
let disp = r#"{"kind":"improvement","tracking_issue":503,"section":"HGVS §x",
"ferro_output":"Z","cluster":"pref"}"#;
let fixture = parse(clusters, disp);
assert_eq!(fixture.cluster_refs(), vec![("X", "pref")]);
assert!(fixture.validate_clusters().is_ok());
let summary = fixture.to_summary();
assert_eq!(summary.rows.len(), 1);
let row = &summary.rows[0];
assert_eq!(row.kind, DispositionKind::Improvement);
assert_eq!(row.axis, "normalized");
assert_eq!(row.ferro_output.as_deref(), Some("Z"));
assert_eq!(row.tracking_issue, Some(503));
assert_eq!(row.cluster.as_deref(), Some("pref"));
}
#[test]
fn dangling_cluster_ref_is_rejected() {
let disp =
r#"{"kind":"known_bug","tracking_issue":472,"ferro_output":"Z","cluster":"missing"}"#;
let err = parse("", disp)
.validate_clusters()
.expect_err("a dangling cluster ref must be rejected");
assert!(err.contains("missing"), "{err}");
}
#[test]
fn disposition_without_cluster_is_ok() {
let disp = r#"{"kind":"known_bug","tracking_issue":472,"ferro_output":"Z"}"#;
let fixture = parse("", disp);
assert!(fixture.cluster_refs().is_empty());
assert!(fixture.validate_clusters().is_ok());
}
}