1use crate::ledger::MemoryStore;
2use crate::trace::{PrivacyClass, Trace};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use unicode_normalization::UnicodeNormalization;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ClaimConflict {
9 pub claim_key: String,
10 pub existing_trace_id: String,
11 pub existing_value: String,
12 pub incoming_value: String,
13}
14
15pub fn detect_conflicts<'a>(
16 incoming: &Trace,
17 existing: impl Iterator<Item = &'a Trace>,
18) -> Vec<ClaimConflict> {
19 let (Some(key), Some(value)) = (&incoming.claim_key, &incoming.claim_value) else {
20 return Vec::new();
21 };
22 let normalized_key = normalize_key(key);
23 let normalized_value = normalize_value(value);
24 existing
25 .filter_map(|trace| {
26 let (Some(existing_key), Some(existing_value)) = (&trace.claim_key, &trace.claim_value)
27 else {
28 return None;
29 };
30 if normalize_key(existing_key) == normalized_key
31 && normalize_value(existing_value) != normalized_value
32 {
33 Some(ClaimConflict {
34 claim_key: key.clone(),
35 existing_trace_id: trace.id.clone(),
36 existing_value: existing_value.clone(),
37 incoming_value: value.clone(),
38 })
39 } else {
40 None
41 }
42 })
43 .collect()
44}
45
46pub fn conflicting_ids<'a>(trace: &Trace, others: impl Iterator<Item = &'a Trace>) -> Vec<String> {
49 let mut ids: Vec<String> = detect_conflicts(trace, others.filter(|other| other.id != trace.id))
50 .into_iter()
51 .map(|conflict| conflict.existing_trace_id)
52 .collect();
53 ids.sort();
54 ids.dedup();
55 ids
56}
57
58pub fn claims_conflict(a: &Trace, b: &Trace) -> bool {
60 match (&a.claim_key, &a.claim_value, &b.claim_key, &b.claim_value) {
61 (Some(ak), Some(av), Some(bk), Some(bv)) => {
62 normalize_key(ak) == normalize_key(bk) && normalize_value(av) != normalize_value(bv)
63 }
64 _ => false,
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ClaimValueGroup {
71 pub value: String,
73 pub trace_ids: Vec<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ActiveConflict {
80 pub claim_key: String,
82 pub values: Vec<ClaimValueGroup>,
83}
84
85impl MemoryStore {
86 pub fn active_conflicts(&self) -> Vec<ActiveConflict> {
90 let allow_sensitive = self.policy().allow_sensitive_persistence;
91 let mut keys: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new();
92 for trace in self.active_traces() {
93 if !allow_sensitive && trace.privacy == PrivacyClass::Sensitive {
94 continue;
95 }
96 let (Some(key), Some(value)) = (&trace.claim_key, &trace.claim_value) else {
97 continue;
98 };
99 keys.entry(normalize_key(key))
100 .or_default()
101 .entry(normalize_value(value))
102 .or_default()
103 .push(trace.id.clone());
104 }
105 keys.into_iter()
106 .filter(|(_, values)| values.len() > 1)
107 .map(|(claim_key, values)| ActiveConflict {
108 claim_key,
109 values: values
110 .into_iter()
111 .map(|(value, trace_ids)| ClaimValueGroup { value, trace_ids })
112 .collect(),
113 })
114 .collect()
115 }
116}
117
118pub fn normalize_key(value: &str) -> String {
120 collapse(&value.nfc().collect::<String>()).to_lowercase()
121}
122
123pub fn normalize_value(value: &str) -> String {
126 collapse(&value.nfc().collect::<String>())
127}
128
129fn collapse(value: &str) -> String {
130 value.split_whitespace().collect::<Vec<_>>().join(" ")
131}