zer_blocking/keys/
document.rs1use zer_core::{record::Record, schema::Schema};
2
3use super::BlockingKey;
4use crate::normalize::normalize_digits_only;
5
6pub struct DocumentSuffixKey {
17 field: String,
18 suffix_len: usize,
19}
20
21impl DocumentSuffixKey {
22 pub fn new(field: &str, suffix_len: usize) -> Self {
24 Self {
25 field: field.into(),
26 suffix_len,
27 }
28 }
29}
30
31impl BlockingKey for DocumentSuffixKey {
32 fn name(&self) -> &str {
33 "document_suffix"
34 }
35
36 fn extract(&self, record: &Record, _schema: &Schema) -> Vec<String> {
37 let cow = record.field_as_str(&self.field);
38 let raw = match cow.as_deref() {
39 Some(s) => s,
40 None => return vec![],
41 };
42 let clean: String = raw
43 .chars()
44 .filter(|c| c.is_ascii_alphanumeric())
45 .collect::<String>()
46 .to_ascii_uppercase();
47 if clean.len() < self.suffix_len {
48 return vec![];
49 }
50 let suffix = &clean[clean.len() - self.suffix_len..];
51 vec![suffix.to_string()]
52 }
53}
54
55pub struct DocumentDigitSuffixKey {
62 field: String,
63 suffix_len: usize,
64}
65
66impl DocumentDigitSuffixKey {
67 pub fn new(field: &str, suffix_len: usize) -> Self {
68 Self {
69 field: field.into(),
70 suffix_len,
71 }
72 }
73}
74
75impl BlockingKey for DocumentDigitSuffixKey {
76 fn name(&self) -> &str {
77 "document_digit_suffix"
78 }
79
80 fn extract(&self, record: &Record, _schema: &Schema) -> Vec<String> {
81 let cow = record.field_as_str(&self.field);
82 let raw = match cow.as_deref() {
83 Some(s) => s,
84 None => return vec![],
85 };
86 let digits = normalize_digits_only(raw);
87 if digits.len() < self.suffix_len {
88 return vec![];
89 }
90 let suffix = &digits[digits.len() - self.suffix_len..];
91 vec![suffix.to_string()]
92 }
93}
94
95#[cfg(test)]
98mod tests {
99 use super::*;
100 use zer_core::{
101 record::FieldValue,
102 schema::{FieldKind, SchemaBuilder},
103 };
104
105 fn schema() -> Schema {
106 SchemaBuilder::new()
107 .field("document_nummer", FieldKind::Id)
108 .build()
109 .unwrap()
110 }
111
112 fn rec(id: u64, doc: &str) -> Record {
113 Record::new(id).insert("document_nummer", FieldValue::Text(doc.into()))
114 }
115
116 #[test]
119 fn suffix_key_strips_non_alphanum_and_uppercases() {
120 let schema = schema();
121 let key = DocumentSuffixKey::new("document_nummer", 6);
122 let r = rec(1, "P-NL-AB123456");
123 let keys = key.extract(&r, &schema);
124 assert_eq!(keys, vec!["123456"]);
125 }
126
127 #[test]
128 fn suffix_key_same_serial_different_prefix_collide() {
129 let schema = schema();
130 let key = DocumentSuffixKey::new("document_nummer", 6);
131
132 let r1 = rec(1, "P-NL-AB123456");
133 let r2 = rec(2, "AB123456");
134 assert_eq!(key.extract(&r1, &schema), key.extract(&r2, &schema));
135 }
136
137 #[test]
138 fn suffix_key_too_short_returns_empty() {
139 let schema = schema();
140 let key = DocumentSuffixKey::new("document_nummer", 6);
141 let r = rec(1, "AB12"); assert!(key.extract(&r, &schema).is_empty());
143 }
144
145 #[test]
146 fn suffix_key_missing_field_returns_empty() {
147 let schema = schema();
148 let key = DocumentSuffixKey::new("document_nummer", 6);
149 assert!(key.extract(&Record::new(1), &schema).is_empty());
150 }
151
152 #[test]
155 fn digit_suffix_strips_all_letters() {
156 let schema = schema();
157 let key = DocumentDigitSuffixKey::new("document_nummer", 4);
158 let r = rec(1, "BSN-12345678");
159 let keys = key.extract(&r, &schema);
160 assert_eq!(keys, vec!["5678"]);
161 }
162
163 #[test]
164 fn digit_suffix_same_number_different_format_collide() {
165 let schema = schema();
166 let key = DocumentDigitSuffixKey::new("document_nummer", 6);
167
168 let r1 = rec(1, "123-45-6789");
169 let r2 = rec(2, "123456789");
170 assert_eq!(key.extract(&r1, &schema), key.extract(&r2, &schema));
171 }
172}