use std::collections::HashMap;
use crate::{tabular_v1, ArtifactFormat, BinocError, BinocResult, TabularData};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct IdentityToken(pub String);
impl IdentityToken {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct IdentityExtractorDescriptor {
pub name: String,
pub format: ArtifactFormat,
}
pub trait IdentityExtractor: Send + Sync {
fn descriptor(&self) -> IdentityExtractorDescriptor;
fn extract(&self, artifact_bytes: &[u8]) -> BinocResult<Vec<IdentityToken>>;
}
pub struct TabularIdentityExtractor;
impl IdentityExtractor for TabularIdentityExtractor {
fn descriptor(&self) -> IdentityExtractorDescriptor {
IdentityExtractorDescriptor {
name: "binoc.identity.tabular".into(),
format: tabular_v1(),
}
}
fn extract(&self, artifact_bytes: &[u8]) -> BinocResult<Vec<IdentityToken>> {
let table: TabularData = serde_json::from_slice(artifact_bytes).map_err(|err| {
BinocError::Other(format!("decode tabular artifact for identity: {err}"))
})?;
Ok(table
.rows
.iter()
.map(|row| {
let canonical = serde_json::to_string(row).unwrap_or_default();
IdentityToken::new(canonical)
})
.collect())
}
}
pub struct Candidate<T> {
pub node: T,
pub tokens: Vec<IdentityToken>,
}
pub struct PartitionMatch<T> {
pub whole: T,
pub parts: Vec<T>,
pub covered: usize,
}
pub enum Coverage<T> {
Clean(PartitionMatch<T>),
NearMiss,
None,
}
pub fn disjoint_cover<T: Clone>(whole: &Candidate<T>, pool: &[Candidate<T>]) -> Coverage<T> {
if whole.tokens.is_empty() {
return Coverage::None;
}
let mut whole_counts: HashMap<&IdentityToken, usize> = HashMap::new();
for token in &whole.tokens {
*whole_counts.entry(token).or_default() += 1;
}
let participants: Vec<usize> = pool
.iter()
.enumerate()
.filter(|(_, cand)| {
!cand.tokens.is_empty() && cand.tokens.iter().any(|t| whole_counts.contains_key(t))
})
.map(|(index, _)| index)
.collect();
if participants.is_empty() {
return Coverage::None;
}
let mut owner: HashMap<&IdentityToken, usize> = HashMap::new();
let mut covered: HashMap<&IdentityToken, usize> = HashMap::new();
let mut clean = true;
for &index in &participants {
for token in &pool[index].tokens {
if !whole_counts.contains_key(token) {
clean = false; }
match owner.get(token) {
Some(&existing) if existing != index => clean = false, _ => {
owner.insert(token, index);
}
}
*covered.entry(token).or_default() += 1;
}
}
if clean {
for (token, &want) in &whole_counts {
if covered.get(token).copied().unwrap_or(0) != want {
clean = false; break;
}
}
}
if participants.len() < 2 {
return Coverage::None;
}
if !clean {
return Coverage::NearMiss;
}
Coverage::Clean(PartitionMatch {
whole: whole.node.clone(),
parts: participants
.iter()
.map(|&index| pool[index].node.clone())
.collect(),
covered: whole.tokens.len(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn cand(node: &str, rows: &[&str]) -> Candidate<String> {
Candidate {
node: node.to_string(),
tokens: rows.iter().map(|r| IdentityToken::new(*r)).collect(),
}
}
#[test]
fn clean_split_is_detected() {
let whole = cand("all", &["a", "b", "c", "d"]);
let pool = vec![cand("x", &["a", "b"]), cand("y", &["c", "d"])];
match disjoint_cover(&whole, &pool) {
Coverage::Clean(m) => {
assert_eq!(m.covered, 4);
assert_eq!(m.parts.len(), 2);
}
_ => panic!("expected clean split"),
}
}
#[test]
fn residual_is_near_miss() {
let whole = cand("all", &["a", "b", "c", "d"]);
let pool = vec![cand("x", &["a", "b"]), cand("y", &["c"])];
assert!(matches!(disjoint_cover(&whole, &pool), Coverage::NearMiss));
}
#[test]
fn shared_token_is_ambiguous_near_miss() {
let whole = cand("all", &["a", "b", "c"]);
let pool = vec![cand("x", &["a", "b"]), cand("y", &["b", "c"])];
assert!(matches!(disjoint_cover(&whole, &pool), Coverage::NearMiss));
}
#[test]
fn foreign_atom_is_near_miss() {
let whole = cand("all", &["a", "b", "c"]);
let pool = vec![cand("x", &["a"]), cand("y", &["b", "c", "z"])];
assert!(matches!(disjoint_cover(&whole, &pool), Coverage::NearMiss));
}
#[test]
fn single_participant_partial_is_not_a_split() {
let whole = cand("data.csv", &["a", "b", "c"]);
let pool = vec![cand("data.tsv", &["a", "c", "d"])];
assert!(matches!(disjoint_cover(&whole, &pool), Coverage::None));
}
#[test]
fn unrelated_pool_is_none() {
let whole = cand("all", &["a", "b"]);
let pool = vec![cand("x", &["m", "n"])];
assert!(matches!(disjoint_cover(&whole, &pool), Coverage::None));
}
#[test]
fn single_whole_cover_is_not_a_split() {
let whole = cand("all", &["a", "b"]);
let pool = vec![cand("x", &["a", "b"])];
assert!(matches!(disjoint_cover(&whole, &pool), Coverage::None));
}
}