#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
Static(String),
Hole,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Guard {
Prefix(String),
Suffix(String),
Contains(String),
}
impl Guard {
pub fn matches(&self, msgid: &str) -> bool {
match self {
Guard::Prefix(p) => msgid.starts_with(p.as_str()),
Guard::Suffix(s) => msgid.ends_with(s.as_str()),
Guard::Contains(c) => msgid.contains(c.as_str()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GuardExtraction {
pub guards: Vec<Guard>,
pub blind: bool,
}
pub fn guards_from_segments(segments: &[Segment], min_guard_len: usize) -> GuardExtraction {
let last = segments.len().saturating_sub(1);
let mut guards = Vec::new();
for (i, segment) in segments.iter().enumerate() {
let Segment::Static(text) = segment else {
continue;
};
if text.chars().count() < min_guard_len {
continue;
}
let at_start = i == 0;
let at_end = i == last;
let guard = match (at_start, at_end) {
(true, true) => Guard::Contains(text.clone()),
(true, false) => Guard::Prefix(text.clone()),
(false, true) => Guard::Suffix(text.clone()),
(false, false) => Guard::Contains(text.clone()),
};
guards.push(guard);
}
let blind = guards.is_empty();
GuardExtraction { guards, blind }
}
#[cfg(test)]
mod tests {
use super::*;
fn s(text: &str) -> Segment {
Segment::Static(text.to_string())
}
#[test]
fn leading_fragment_becomes_prefix() {
let segs = [s("cf_subtype_"), Segment::Hole];
let ext = guards_from_segments(&segs, 3);
assert_eq!(ext.guards, vec![Guard::Prefix("cf_subtype_".to_string())]);
assert!(!ext.blind);
}
#[test]
fn trailing_fragment_becomes_suffix() {
let segs = [Segment::Hole, s("_delete_confirm_1")];
let ext = guards_from_segments(&segs, 3);
assert_eq!(
ext.guards,
vec![Guard::Suffix("_delete_confirm_1".to_string())]
);
assert!(!ext.blind);
}
#[test]
fn interior_fragment_becomes_contains() {
let segs = [Segment::Hole, s("_middle_"), Segment::Hole];
let ext = guards_from_segments(&segs, 3);
assert_eq!(ext.guards, vec![Guard::Contains("_middle_".to_string())]);
}
#[test]
fn short_fragment_yields_no_guard_and_is_blind() {
let segs = [Segment::Hole, s("_"), Segment::Hole];
let ext = guards_from_segments(&segs, 3);
assert!(ext.guards.is_empty());
assert!(ext.blind);
}
#[test]
fn pure_variable_is_blind() {
let segs = [Segment::Hole];
let ext = guards_from_segments(&segs, 3);
assert!(ext.guards.is_empty());
assert!(ext.blind);
}
#[test]
fn fragment_at_threshold_is_kept() {
let segs = [s("cf_"), Segment::Hole];
let ext = guards_from_segments(&segs, 3);
assert_eq!(ext.guards, vec![Guard::Prefix("cf_".to_string())]);
}
#[test]
fn guard_matching() {
let prefix = Guard::Prefix("cf_subtype_".to_string());
assert!(prefix.matches("cf_subtype_foo"));
assert!(!prefix.matches("other_key"));
let suffix = Guard::Suffix("_delete_confirm_1".to_string());
assert!(suffix.matches("entity_delete_confirm_1"));
assert!(!suffix.matches("entity_delete_confirm_2"));
let contains = Guard::Contains("_middle_".to_string());
assert!(contains.matches("a_middle_b"));
assert!(!contains.matches("a_b"));
}
}