151 lines
4.8 KiB
Rust
151 lines
4.8 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
/// Parsed mention from `@[type:id:label]` IR format.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct Mention {
|
|
pub mention_type: String,
|
|
pub target_id: String,
|
|
pub label: String,
|
|
}
|
|
|
|
/// Parse all `@[type:id:label]` mentions from content.
|
|
/// Returns deduplicated mentions in order of first appearance.
|
|
pub fn parse_mentions(content: &str) -> Vec<Mention> {
|
|
let mut mentions = Vec::new();
|
|
let mut seen = std::collections::HashSet::new();
|
|
|
|
// Simple manual parser for @[type:id:label]
|
|
let bytes = content.as_bytes();
|
|
let len = bytes.len();
|
|
let mut i = 0;
|
|
|
|
while i < len {
|
|
// Look for "@["
|
|
if i + 2 < len && bytes[i] == b'@' && bytes[i + 1] == b'[' {
|
|
let start = i + 2; // after "@["
|
|
|
|
// Find first ':'' after start
|
|
if let Some(type_end) = content[start..].find(':') {
|
|
let mention_type = &content[start..start + type_end];
|
|
let after_type = start + type_end + 1; // after first ':'
|
|
|
|
// Find second ':' (between id and label)
|
|
if let Some(id_end) = content[after_type..].find(':') {
|
|
let target_id = &content[after_type..after_type + id_end];
|
|
let after_id = after_type + id_end + 1; // after second ':'
|
|
|
|
// Find closing ']'
|
|
if let Some(close) = content[after_id..].find(']') {
|
|
let label = &content[after_id..after_id + close];
|
|
|
|
if !mention_type.is_empty() && !target_id.is_empty() {
|
|
let key = format!("{}:{}", mention_type, target_id);
|
|
if seen.insert(key) {
|
|
mentions.push(Mention {
|
|
mention_type: mention_type.to_string(),
|
|
target_id: target_id.to_string(),
|
|
label: label.to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
i = after_id + close + 1; // skip past ']'
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
mentions
|
|
}
|
|
|
|
/// Extract unique target IDs of a specific mention type.
|
|
pub fn extract_mention_ids(
|
|
mentions: &[Mention],
|
|
mention_type: &str,
|
|
) -> Vec<String> {
|
|
mentions
|
|
.iter()
|
|
.filter(|m| m.mention_type == mention_type)
|
|
.map(|m| m.target_id.clone())
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_single_room_mention() {
|
|
let input = "hey check out @[room:abc123:general]";
|
|
let mentions = parse_mentions(input);
|
|
assert_eq!(mentions.len(), 1);
|
|
assert_eq!(mentions[0].mention_type, "room");
|
|
assert_eq!(mentions[0].target_id, "abc123");
|
|
assert_eq!(mentions[0].label, "general");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single_repo_mention() {
|
|
let input = "look at @[repo:my-repo:my-repo]";
|
|
let mentions = parse_mentions(input);
|
|
assert_eq!(mentions.len(), 1);
|
|
assert_eq!(mentions[0].mention_type, "repo");
|
|
assert_eq!(mentions[0].target_id, "my-repo");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_multiple_mentions() {
|
|
let input =
|
|
"compare @[repo:backend:backend] with @[repo:frontend:frontend]";
|
|
let mentions = parse_mentions(input);
|
|
assert_eq!(mentions.len(), 2);
|
|
assert_eq!(mentions[0].target_id, "backend");
|
|
assert_eq!(mentions[1].target_id, "frontend");
|
|
}
|
|
|
|
#[test]
|
|
fn test_deduplicate() {
|
|
let input = "look at @[repo:a:a] and also @[repo:a:a] please";
|
|
let mentions = parse_mentions(input);
|
|
assert_eq!(mentions.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_mentions() {
|
|
let input = "hello world no mentions here";
|
|
let mentions = parse_mentions(input);
|
|
assert_eq!(mentions.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_mentions() {
|
|
let input = "@[user:abc:John] and @[room:xyz:general] and @[repo:r:r]";
|
|
let mentions = parse_mentions(input);
|
|
assert_eq!(mentions.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_incomplete_mention_ignored() {
|
|
let input = "this @[incomplete is just text";
|
|
let mentions = parse_mentions(input);
|
|
assert_eq!(mentions.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_input() {
|
|
let mentions = parse_mentions("");
|
|
assert_eq!(mentions.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_mention_ids() {
|
|
let input = "@[repo:a:a] and @[room:b:general] and @[repo:c:c]";
|
|
let mentions = parse_mentions(input);
|
|
let repo_ids = extract_mention_ids(&mentions, "repo");
|
|
assert_eq!(repo_ids, vec!["a", "c"]);
|
|
}
|
|
}
|