1use std::collections::BTreeMap;
5use std::fmt;
6use std::str::FromStr;
7
8use crate::MATCHER_V;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct MatchBlock {
16 pub id: Option<String>,
18 pub kind: String,
20 pub raw_hash: String,
22 pub norm_hash: String,
24 pub text: String,
26 pub anchors: Vec<String>,
28 pub parent_key: Option<String>,
30 pub index: usize,
32 pub key: String,
34}
35
36impl MatchBlock {
37 #[must_use]
39 pub fn positional_key(parent_key: Option<&str>, index: usize) -> String {
40 format!("{}/{index}", parent_key.unwrap_or(""))
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct UnknownName(pub String);
47
48impl fmt::Display for UnknownName {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "unknown name {:?}", self.0)
51 }
52}
53
54impl std::error::Error for UnknownName {}
55
56macro_rules! spec_enum {
57 ($(#[$meta:meta])* $name:ident { $($variant:ident => $s:literal),+ $(,)? }) => {
58 $(#[$meta])*
59 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
60 pub enum $name {
61 $($variant),+
62 }
63
64 impl $name {
65 pub const ALL: &'static [$name] = &[$($name::$variant),+];
67
68 #[must_use]
70 pub const fn as_str(&self) -> &'static str {
71 match self {
72 $($name::$variant => $s),+
73 }
74 }
75 }
76
77 impl fmt::Display for $name {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.write_str(self.as_str())
80 }
81 }
82
83 impl FromStr for $name {
84 type Err = UnknownName;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 Self::ALL
88 .iter()
89 .copied()
90 .find(|v| v.as_str() == s)
91 .ok_or_else(|| UnknownName(s.to_owned()))
92 }
93 }
94 };
95}
96
97spec_enum! {
98 DispositionKind {
100 Same => "same",
101 Edited => "edited",
102 Moved => "moved",
103 EditedMoved => "edited_moved",
104 Inserted => "inserted",
105 Deleted => "deleted",
106 SplitFrom => "split_from",
107 MergedInto => "merged_into",
108 CopiedFrom => "copied_from",
109 Resurrected => "resurrected",
110 BulkRewrite => "bulk_rewrite",
111 }
112}
113
114spec_enum! {
115 Reason {
119 ExactHash => "exact_hash",
120 NormalizedHash => "normalized_hash",
121 Anchor => "anchor",
122 ContextUnique => "context_unique",
123 ContextChildren => "context_children",
124 Scored => "scored",
125 Tombstone => "tombstone",
126 Api => "api",
127 }
128}
129
130pub type Detail = BTreeMap<String, DetailValue>;
134
135#[derive(Clone, Debug, PartialEq)]
141pub enum DetailValue {
142 Str(String),
143 Int(i64),
144 Num(f64),
145 List(Vec<DetailValue>),
146 Map(Detail),
147}
148
149impl From<&str> for DetailValue {
150 fn from(s: &str) -> Self {
151 DetailValue::Str(s.to_owned())
152 }
153}
154
155impl From<String> for DetailValue {
156 fn from(s: String) -> Self {
157 DetailValue::Str(s)
158 }
159}
160
161impl From<i64> for DetailValue {
162 fn from(n: i64) -> Self {
163 DetailValue::Int(n)
164 }
165}
166
167impl From<f64> for DetailValue {
168 fn from(n: f64) -> Self {
169 DetailValue::Num(n)
170 }
171}
172
173impl From<Vec<DetailValue>> for DetailValue {
174 fn from(list: Vec<DetailValue>) -> Self {
175 DetailValue::List(list)
176 }
177}
178
179impl From<Detail> for DetailValue {
180 fn from(map: Detail) -> Self {
181 DetailValue::Map(map)
182 }
183}
184
185pub fn detail<V: Into<DetailValue>, const N: usize>(entries: [(&str, V); N]) -> Detail {
187 entries
188 .into_iter()
189 .map(|(k, v)| (k.to_owned(), v.into()))
190 .collect()
191}
192
193#[derive(Clone, Debug, PartialEq)]
197pub struct Disposition {
198 pub block_id: String,
199 pub kind: DispositionKind,
200 pub confidence: Option<f64>,
202 pub reason: Option<Reason>,
203 pub matcher_v: String,
205 pub detail: Detail,
206}
207
208#[derive(Clone, Debug, PartialEq)]
211pub struct Config {
212 pub matcher_v: String,
214 pub theta_accept: f64,
216 pub theta_small: f64,
219 pub small_block_tokens: usize,
221 pub context_sim_floor: f64,
223 pub children_vouch_frac: f64,
226 pub split_coverage: f64,
228 pub split_dominant_share: f64,
230 pub copy_sim: f64,
232 pub bulk_unmatched_frac: f64,
234 pub bulk_min_blocks: usize,
236 pub max_scored_blocks: usize,
238 pub theta_xdoc: f64,
240}
241
242impl Default for Config {
243 fn default() -> Self {
245 Self {
246 matcher_v: MATCHER_V.to_owned(),
247 theta_accept: 0.62,
248 theta_small: 0.62,
249 small_block_tokens: 8,
250 context_sim_floor: 0.35,
251 children_vouch_frac: 0.5,
252 split_coverage: 0.8,
253 split_dominant_share: 0.7,
254 copy_sim: 0.95,
255 bulk_unmatched_frac: 0.45,
256 bulk_min_blocks: 100,
257 max_scored_blocks: 2000,
258 theta_xdoc: 0.8,
259 }
260 }
261}
262
263#[derive(Clone, Debug, PartialEq, Eq)]
266pub struct PoolEntry {
267 pub id: String,
268 pub kind: String,
270 pub raw_hash: String,
272 pub norm_hash: String,
274}
275
276#[derive(Clone, Debug, PartialEq, Default)]
278pub struct ReconcileResult {
279 pub assignment: BTreeMap<String, String>,
282 pub dispositions: Vec<Disposition>,
284 pub deleted: Vec<String>,
287 pub consumed_pool: Vec<String>,
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn enum_names_round_trip() {
297 for k in DispositionKind::ALL {
298 assert_eq!(k.as_str().parse::<DispositionKind>(), Ok(*k));
299 assert_eq!(k.to_string(), k.as_str());
300 }
301 for r in Reason::ALL {
302 assert_eq!(r.as_str().parse::<Reason>(), Ok(*r));
303 }
304 assert_eq!(
305 "renamed".parse::<DispositionKind>(),
306 Err(UnknownName("renamed".to_owned()))
307 );
308 assert_eq!(DispositionKind::ALL.len(), 11);
309 }
310
311 #[test]
312 fn positional_keys() {
313 assert_eq!(MatchBlock::positional_key(None, 0), "/0");
314 assert_eq!(MatchBlock::positional_key(Some("/1/2"), 0), "/1/2/0");
315 }
316
317 #[test]
318 fn detail_builder() {
319 let d = detail([("a", DetailValue::Int(1)), ("b", "x".into())]);
320 assert_eq!(d.len(), 2);
321 assert_eq!(d["b"], DetailValue::Str("x".to_owned()));
322 assert_eq!(Detail::new(), detail::<DetailValue, 0>([]));
323 }
324}