use crate::ledger::MemoryStore;
use crate::trace::{PrivacyClass, Trace};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use unicode_normalization::UnicodeNormalization;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimConflict {
pub claim_key: String,
pub existing_trace_id: String,
pub existing_value: String,
pub incoming_value: String,
}
pub fn detect_conflicts<'a>(
incoming: &Trace,
existing: impl Iterator<Item = &'a Trace>,
) -> Vec<ClaimConflict> {
let (Some(key), Some(value)) = (&incoming.claim_key, &incoming.claim_value) else {
return Vec::new();
};
let normalized_key = normalize_key(key);
let normalized_value = normalize_value(value);
existing
.filter_map(|trace| {
let (Some(existing_key), Some(existing_value)) = (&trace.claim_key, &trace.claim_value)
else {
return None;
};
if normalize_key(existing_key) == normalized_key
&& normalize_value(existing_value) != normalized_value
{
Some(ClaimConflict {
claim_key: key.clone(),
existing_trace_id: trace.id.clone(),
existing_value: existing_value.clone(),
incoming_value: value.clone(),
})
} else {
None
}
})
.collect()
}
pub fn conflicting_ids<'a>(trace: &Trace, others: impl Iterator<Item = &'a Trace>) -> Vec<String> {
let mut ids: Vec<String> = detect_conflicts(trace, others.filter(|other| other.id != trace.id))
.into_iter()
.map(|conflict| conflict.existing_trace_id)
.collect();
ids.sort();
ids.dedup();
ids
}
pub fn claims_conflict(a: &Trace, b: &Trace) -> bool {
match (&a.claim_key, &a.claim_value, &b.claim_key, &b.claim_value) {
(Some(ak), Some(av), Some(bk), Some(bv)) => {
normalize_key(ak) == normalize_key(bk) && normalize_value(av) != normalize_value(bv)
}
_ => false,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimValueGroup {
pub value: String,
pub trace_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveConflict {
pub claim_key: String,
pub values: Vec<ClaimValueGroup>,
}
impl MemoryStore {
pub fn active_conflicts(&self) -> Vec<ActiveConflict> {
let allow_sensitive = self.policy().allow_sensitive_persistence;
let mut keys: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new();
for trace in self.active_traces() {
if !allow_sensitive && trace.privacy == PrivacyClass::Sensitive {
continue;
}
let (Some(key), Some(value)) = (&trace.claim_key, &trace.claim_value) else {
continue;
};
keys.entry(normalize_key(key))
.or_default()
.entry(normalize_value(value))
.or_default()
.push(trace.id.clone());
}
keys.into_iter()
.filter(|(_, values)| values.len() > 1)
.map(|(claim_key, values)| ActiveConflict {
claim_key,
values: values
.into_iter()
.map(|(value, trace_ids)| ClaimValueGroup { value, trace_ids })
.collect(),
})
.collect()
}
}
pub fn normalize_key(value: &str) -> String {
collapse(&value.nfc().collect::<String>()).to_lowercase()
}
pub fn normalize_value(value: &str) -> String {
collapse(&value.nfc().collect::<String>())
}
fn collapse(value: &str) -> String {
value.split_whitespace().collect::<Vec<_>>().join(" ")
}