use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentRoute {
pub channel: String,
#[serde(rename = "match", default)]
pub match_criteria: MatchCriteria,
pub agent: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MatchCriteria {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_id: Option<String>,
}
impl MatchCriteria {
pub fn matches(&self, sender_id: &str, chat_id: &str) -> bool {
if let Some(ref uid) = self.user_id
&& uid != sender_id
{
return false;
}
if let Some(ref phone) = self.phone
&& phone != sender_id
&& phone != chat_id
{
return false;
}
if let Some(ref cid) = self.chat_id
&& cid != chat_id
{
return false;
}
true
}
pub fn is_empty(&self) -> bool {
self.user_id.is_none() && self.phone.is_none() && self.chat_id.is_none()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentRoutingConfig {
#[serde(default)]
pub routes: Vec<AgentRoute>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catch_all: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn match_criteria_empty_matches_all() {
let criteria = MatchCriteria::default();
assert!(criteria.is_empty());
assert!(criteria.matches("any_user", "any_chat"));
}
#[test]
fn match_criteria_user_id() {
let criteria = MatchCriteria {
user_id: Some("user123".into()),
..Default::default()
};
assert!(criteria.matches("user123", "chat1"));
assert!(!criteria.matches("other_user", "chat1"));
}
#[test]
fn match_criteria_phone() {
let criteria = MatchCriteria {
phone: Some("+1234567890".into()),
..Default::default()
};
assert!(criteria.matches("+1234567890", "chat1"));
assert!(criteria.matches("other", "+1234567890"));
assert!(!criteria.matches("other", "chat1"));
}
#[test]
fn match_criteria_chat_id() {
let criteria = MatchCriteria {
chat_id: Some("chat42".into()),
..Default::default()
};
assert!(criteria.matches("any_user", "chat42"));
assert!(!criteria.matches("any_user", "other_chat"));
}
#[test]
fn match_criteria_combined_and() {
let criteria = MatchCriteria {
user_id: Some("user1".into()),
chat_id: Some("chat1".into()),
..Default::default()
};
assert!(criteria.matches("user1", "chat1"));
assert!(!criteria.matches("user2", "chat1"));
assert!(!criteria.matches("user1", "chat2"));
}
#[test]
fn agent_route_serde_roundtrip() {
let route = AgentRoute {
channel: "telegram".into(),
match_criteria: MatchCriteria {
user_id: Some("12345".into()),
..Default::default()
},
agent: "work-agent".into(),
};
let json = serde_json::to_string(&route).unwrap();
let restored: AgentRoute = serde_json::from_str(&json).unwrap();
assert_eq!(restored.channel, "telegram");
assert_eq!(restored.agent, "work-agent");
assert_eq!(restored.match_criteria.user_id.as_deref(), Some("12345"));
}
#[test]
fn agent_routing_config_defaults() {
let cfg = AgentRoutingConfig::default();
assert!(cfg.routes.is_empty());
assert!(cfg.catch_all.is_none());
}
#[test]
fn agent_routing_config_serde_with_catch_all() {
let json = r#"{
"routes": [
{"channel": "telegram", "match": {"user_id": "123"}, "agent": "bot-a"},
{"channel": "slack", "agent": "bot-b"}
],
"catch_all": "default-bot"
}"#;
let cfg: AgentRoutingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.routes.len(), 2);
assert_eq!(cfg.routes[0].agent, "bot-a");
assert_eq!(cfg.routes[1].agent, "bot-b");
assert_eq!(cfg.catch_all.as_deref(), Some("default-bot"));
}
}