use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::LazyLock;
use super::entry::{BindingEntry, BindingSpec};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Mention {
Number(u32),
Last,
All,
Range { start: u32, end: u32 },
}
impl fmt::Display for Mention {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Mention::Number(n) => write!(f, "@{}", n),
Mention::Last => write!(f, "@last"),
Mention::All => write!(f, "@all"),
Mention::Range { start, end } => write!(f, "@{}..{}", start, end),
}
}
}
static MENTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?:^|[\s\[\](),:;])@(?:(\d+)\.\.(\d+)|(\d+)|(last|all))")
.expect("Invalid mention regex")
});
pub fn parse_mentions(text: &str) -> Vec<Mention> {
let mut mentions = Vec::new();
for cap in MENTION_REGEX.captures_iter(text) {
if let (Some(start), Some(end)) = (cap.get(1), cap.get(2)) {
let Ok(start) = start.as_str().parse::<u32>() else {
continue;
};
let Ok(end) = end.as_str().parse::<u32>() else {
continue;
};
mentions.push(Mention::Range { start, end });
} else if let Some(num) = cap.get(3) {
let Ok(n) = num.as_str().parse::<u32>() else {
continue;
};
mentions.push(Mention::Number(n));
} else if let Some(keyword) = cap.get(4) {
match keyword.as_str() {
"last" => mentions.push(Mention::Last),
"all" => mentions.push(Mention::All),
_ => {}
}
}
}
mentions
}
pub fn has_parallel_marker(text: &str) -> bool {
text.trim_start().starts_with("//")
}
pub fn strip_parallel_marker(text: &str) -> &str {
let trimmed = text.trim_start();
if let Some(stripped) = trimmed.strip_prefix("//") {
stripped.trim_start()
} else {
text
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedMention {
Single(u32),
Multiple(Vec<u32>),
Empty,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MentionResolutionError {
MessageNotFound { index: u32, max: u32 },
InvalidRange { start: u32, end: u32 },
NoMessages,
}
impl std::fmt::Display for MentionResolutionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MessageNotFound { index, max } => {
write!(f, "Message @{} not found (max: @{})", index, max)
}
Self::InvalidRange { start, end } => {
write!(f, "Invalid range @{}..{} (start > end)", start, end)
}
Self::NoMessages => write!(f, "No messages to reference"),
}
}
}
impl std::error::Error for MentionResolutionError {}
pub fn resolve_mention(
mention: &Mention,
message_count: u32,
) -> Result<ResolvedMention, MentionResolutionError> {
match mention {
Mention::Last => {
if message_count == 0 {
Err(MentionResolutionError::NoMessages)
} else {
Ok(ResolvedMention::Single(message_count))
}
}
Mention::Number(n) => {
if *n == 0 || *n > message_count {
Err(MentionResolutionError::MessageNotFound {
index: *n,
max: message_count,
})
} else {
Ok(ResolvedMention::Single(*n))
}
}
Mention::All => {
if message_count == 0 {
Ok(ResolvedMention::Empty)
} else {
Ok(ResolvedMention::Multiple((1..=message_count).collect()))
}
}
Mention::Range { start, end } => {
if start > end {
return Err(MentionResolutionError::InvalidRange {
start: *start,
end: *end,
});
}
if *start == 0 || *end > message_count {
return Err(MentionResolutionError::MessageNotFound {
index: if *start == 0 { 0 } else { *end },
max: message_count,
});
}
Ok(ResolvedMention::Multiple((*start..=*end).collect()))
}
}
}
pub fn mentions_to_bindings(resolved: &ResolvedMention) -> BindingSpec {
let mut spec = BindingSpec::default();
match resolved {
ResolvedMention::Single(n) => {
let alias = format!("ref_{}", n);
let path = format!("msg-{:03}.output", n);
spec.insert(alias, BindingEntry::new(path));
}
ResolvedMention::Multiple(indices) => {
for n in indices {
let alias = format!("ref_{}", n);
let path = format!("msg-{:03}.output", n);
spec.insert(alias, BindingEntry::new(path));
}
}
ResolvedMention::Empty => {
}
}
spec
}
pub fn text_to_bindings(
text: &str,
message_count: u32,
) -> Result<BindingSpec, MentionResolutionError> {
let mut spec = BindingSpec::default();
for mention in parse_mentions(text) {
let resolved = resolve_mention(&mention, message_count)?;
spec.extend(mentions_to_bindings(&resolved));
}
Ok(spec)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mention_module_exists() {
let _: Mention = Mention::Number(1);
}
#[test]
fn test_mention_number_stores_value() {
let m = Mention::Number(42);
if let Mention::Number(n) = m {
assert_eq!(n, 42);
} else {
panic!("Expected Number variant");
}
}
#[test]
fn test_mention_range_stores_bounds() {
let m = Mention::Range { start: 1, end: 5 };
if let Mention::Range { start, end } = m {
assert_eq!(start, 1);
assert_eq!(end, 5);
} else {
panic!("Expected Range variant");
}
}
#[test]
fn test_mention_equality() {
assert_eq!(Mention::Number(1), Mention::Number(1));
assert_ne!(Mention::Number(1), Mention::Number(2));
assert_eq!(Mention::Last, Mention::Last);
assert_eq!(Mention::All, Mention::All);
}
#[test]
fn test_mention_clone() {
let m = Mention::Range { start: 1, end: 10 };
let cloned = m.clone();
assert_eq!(m, cloned);
}
#[test]
fn test_mention_serialization() {
let m = Mention::Number(5);
let json = serde_json::to_string(&m).unwrap();
let restored: Mention = serde_json::from_str(&json).unwrap();
assert_eq!(m, restored);
}
#[test]
fn test_mention_display_number() {
assert_eq!(format!("{}", Mention::Number(1)), "@1");
assert_eq!(format!("{}", Mention::Number(42)), "@42");
assert_eq!(format!("{}", Mention::Number(999)), "@999");
}
#[test]
fn test_mention_display_last() {
assert_eq!(format!("{}", Mention::Last), "@last");
}
#[test]
fn test_mention_display_all() {
assert_eq!(format!("{}", Mention::All), "@all");
}
#[test]
fn test_mention_display_range() {
assert_eq!(format!("{}", Mention::Range { start: 1, end: 3 }), "@1..3");
assert_eq!(
format!("{}", Mention::Range { start: 10, end: 20 }),
"@10..20"
);
}
#[test]
fn test_parse_mentions_number() {
let text = "Look at @1 and @2";
let mentions = parse_mentions(text);
assert_eq!(mentions, vec![Mention::Number(1), Mention::Number(2)]);
}
#[test]
fn test_parse_mentions_at_start() {
let text = "@1 is the first message";
let mentions = parse_mentions(text);
assert_eq!(mentions, vec![Mention::Number(1)]);
}
#[test]
fn test_parse_mentions_last() {
let text = "Continue from @last";
let mentions = parse_mentions(text);
assert_eq!(mentions, vec![Mention::Last]);
}
#[test]
fn test_parse_mentions_all() {
let text = "Summarize @all";
let mentions = parse_mentions(text);
assert_eq!(mentions, vec![Mention::All]);
}
#[test]
fn test_parse_mentions_range() {
let text = "Combine @1..3";
let mentions = parse_mentions(text);
assert_eq!(mentions, vec![Mention::Range { start: 1, end: 3 }]);
}
#[test]
fn test_parse_mentions_mixed() {
let text = "Based on @1, @last, and @all";
let mentions = parse_mentions(text);
assert_eq!(
mentions,
vec![Mention::Number(1), Mention::Last, Mention::All,]
);
}
#[test]
fn test_parse_mentions_no_matches() {
let text = "No mentions here";
let mentions = parse_mentions(text);
assert!(mentions.is_empty());
}
#[test]
fn test_parse_mentions_after_punctuation() {
let text = "See (@1) and [@2]";
let mentions = parse_mentions(text);
assert_eq!(mentions, vec![Mention::Number(1), Mention::Number(2)]);
}
#[test]
fn test_parse_mentions_email_not_matched() {
let text = "Contact user@123.com for help";
let mentions = parse_mentions(text);
assert!(mentions.is_empty(), "Email should not be parsed as mention");
}
#[test]
fn test_parse_mentions_mixed_with_email() {
let text = "Check @123 after emailing user@456.com";
let mentions = parse_mentions(text);
assert_eq!(mentions, vec![Mention::Number(123)]);
}
#[test]
fn test_resolve_last_with_messages() {
let result = resolve_mention(&Mention::Last, 3);
assert_eq!(result, Ok(ResolvedMention::Single(3)));
}
#[test]
fn test_resolve_last_with_one_message() {
let result = resolve_mention(&Mention::Last, 1);
assert_eq!(result, Ok(ResolvedMention::Single(1)));
}
#[test]
fn test_resolve_last_with_no_messages() {
let result = resolve_mention(&Mention::Last, 0);
assert_eq!(result, Err(MentionResolutionError::NoMessages));
}
#[test]
fn test_resolve_number_valid() {
let result = resolve_mention(&Mention::Number(2), 3);
assert_eq!(result, Ok(ResolvedMention::Single(2)));
}
#[test]
fn test_resolve_number_first_message() {
let result = resolve_mention(&Mention::Number(1), 5);
assert_eq!(result, Ok(ResolvedMention::Single(1)));
}
#[test]
fn test_resolve_number_out_of_bounds() {
let result = resolve_mention(&Mention::Number(5), 3);
assert_eq!(
result,
Err(MentionResolutionError::MessageNotFound { index: 5, max: 3 })
);
}
#[test]
fn test_resolve_number_zero() {
let result = resolve_mention(&Mention::Number(0), 3);
assert_eq!(
result,
Err(MentionResolutionError::MessageNotFound { index: 0, max: 3 })
);
}
#[test]
fn test_resolve_all_with_messages() {
let result = resolve_mention(&Mention::All, 3);
assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
}
#[test]
fn test_resolve_all_with_one_message() {
let result = resolve_mention(&Mention::All, 1);
assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1])));
}
#[test]
fn test_resolve_all_with_no_messages() {
let result = resolve_mention(&Mention::All, 0);
assert_eq!(result, Ok(ResolvedMention::Empty));
}
#[test]
fn test_resolve_range_valid() {
let result = resolve_mention(&Mention::Range { start: 1, end: 3 }, 5);
assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
}
#[test]
fn test_resolve_range_single() {
let result = resolve_mention(&Mention::Range { start: 2, end: 2 }, 3);
assert_eq!(result, Ok(ResolvedMention::Multiple(vec![2])));
}
#[test]
fn test_resolve_range_full() {
let result = resolve_mention(&Mention::Range { start: 1, end: 3 }, 3);
assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
}
#[test]
fn test_resolve_range_out_of_bounds() {
let result = resolve_mention(&Mention::Range { start: 1, end: 5 }, 3);
assert_eq!(
result,
Err(MentionResolutionError::MessageNotFound { index: 5, max: 3 })
);
}
#[test]
fn test_resolve_range_invalid_order() {
let result = resolve_mention(&Mention::Range { start: 3, end: 1 }, 5);
assert_eq!(
result,
Err(MentionResolutionError::InvalidRange { start: 3, end: 1 })
);
}
#[test]
fn test_resolve_range_zero_start() {
let result = resolve_mention(&Mention::Range { start: 0, end: 3 }, 5);
assert_eq!(
result,
Err(MentionResolutionError::MessageNotFound { index: 0, max: 5 })
);
}
#[test]
fn test_error_display() {
let err = MentionResolutionError::MessageNotFound { index: 5, max: 3 };
assert_eq!(format!("{}", err), "Message @5 not found (max: @3)");
let err = MentionResolutionError::InvalidRange { start: 3, end: 1 };
assert_eq!(format!("{}", err), "Invalid range @3..1 (start > end)");
let err = MentionResolutionError::NoMessages;
assert_eq!(format!("{}", err), "No messages to reference");
}
#[test]
fn test_has_parallel_marker_basic() {
assert!(has_parallel_marker("// Independent task"));
assert!(has_parallel_marker("//Task"));
}
#[test]
fn test_has_parallel_marker_with_whitespace() {
assert!(has_parallel_marker(" // Also parallel"));
assert!(has_parallel_marker("\t// Tab prefixed"));
}
#[test]
fn test_has_parallel_marker_false() {
assert!(!has_parallel_marker("Normal message"));
assert!(!has_parallel_marker("@1 Reference"));
assert!(!has_parallel_marker("/ Single slash"));
assert!(!has_parallel_marker(""));
}
#[test]
fn test_has_parallel_marker_not_url() {
assert!(!has_parallel_marker("https://example.com"));
assert!(!has_parallel_marker("http://localhost"));
}
#[test]
fn test_strip_parallel_marker_basic() {
assert_eq!(strip_parallel_marker("// Task"), "Task");
assert_eq!(strip_parallel_marker("//Task"), "Task");
}
#[test]
fn test_strip_parallel_marker_with_whitespace() {
assert_eq!(
strip_parallel_marker(" // Parallel work"),
"Parallel work"
);
assert_eq!(strip_parallel_marker("\t// Tab"), "Tab");
}
#[test]
fn test_strip_parallel_marker_no_marker() {
assert_eq!(strip_parallel_marker("Normal message"), "Normal message");
assert_eq!(strip_parallel_marker("@1 Reference"), "@1 Reference");
}
#[test]
fn test_strip_parallel_marker_empty() {
assert_eq!(strip_parallel_marker("//"), "");
assert_eq!(strip_parallel_marker("// "), "");
}
#[test]
fn test_mentions_to_bindings_single() {
let spec = mentions_to_bindings(&ResolvedMention::Single(2));
assert_eq!(spec.len(), 1);
assert!(spec.contains_key("ref_2"));
assert_eq!(spec["ref_2"].path, "msg-002.output");
}
#[test]
fn test_mentions_to_bindings_single_large_number() {
let spec = mentions_to_bindings(&ResolvedMention::Single(123));
assert_eq!(spec.len(), 1);
assert!(spec.contains_key("ref_123"));
assert_eq!(spec["ref_123"].path, "msg-123.output");
}
#[test]
fn test_mentions_to_bindings_multiple() {
let spec = mentions_to_bindings(&ResolvedMention::Multiple(vec![1, 2, 3]));
assert_eq!(spec.len(), 3);
assert_eq!(spec["ref_1"].path, "msg-001.output");
assert_eq!(spec["ref_2"].path, "msg-002.output");
assert_eq!(spec["ref_3"].path, "msg-003.output");
}
#[test]
fn test_mentions_to_bindings_empty() {
let spec = mentions_to_bindings(&ResolvedMention::Empty);
assert!(spec.is_empty());
}
#[test]
fn test_mentions_to_bindings_entry_is_eager() {
let spec = mentions_to_bindings(&ResolvedMention::Single(1));
assert!(!spec["ref_1"].lazy);
assert!(spec["ref_1"].default.is_none());
}
#[test]
fn test_text_to_bindings_simple() {
let spec = text_to_bindings("Based on @1", 3).unwrap();
assert_eq!(spec.len(), 1);
assert!(spec.contains_key("ref_1"));
}
#[test]
fn test_text_to_bindings_multiple() {
let spec = text_to_bindings("Combine @1 and @2", 3).unwrap();
assert_eq!(spec.len(), 2);
assert!(spec.contains_key("ref_1"));
assert!(spec.contains_key("ref_2"));
}
#[test]
fn test_text_to_bindings_with_last() {
let spec = text_to_bindings("Continue from @last", 5).unwrap();
assert_eq!(spec.len(), 1);
assert!(spec.contains_key("ref_5")); }
#[test]
fn test_text_to_bindings_with_range() {
let spec = text_to_bindings("Summarize @1..3", 5).unwrap();
assert_eq!(spec.len(), 3);
assert!(spec.contains_key("ref_1"));
assert!(spec.contains_key("ref_2"));
assert!(spec.contains_key("ref_3"));
}
#[test]
fn test_text_to_bindings_with_all() {
let spec = text_to_bindings("Based on @all", 3).unwrap();
assert_eq!(spec.len(), 3);
assert!(spec.contains_key("ref_1"));
assert!(spec.contains_key("ref_2"));
assert!(spec.contains_key("ref_3"));
}
#[test]
fn test_text_to_bindings_no_mentions() {
let spec = text_to_bindings("Just a normal message", 5).unwrap();
assert!(spec.is_empty());
}
#[test]
fn test_text_to_bindings_error_out_of_bounds() {
let result = text_to_bindings("Reference @10", 3);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
MentionResolutionError::MessageNotFound { index: 10, max: 3 }
);
}
#[test]
fn test_text_to_bindings_dedup() {
let spec = text_to_bindings("See @1 and again @1", 3).unwrap();
assert_eq!(spec.len(), 1); assert!(spec.contains_key("ref_1"));
}
}