1use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default)]
10pub struct ProjectChannelConfig {
11 pub telegram_topic_id: Option<String>,
12 pub discord_channel_id: Option<String>,
13}
14
15#[derive(Debug, Clone, Default)]
17pub struct GlobalChannelConfig {
18 pub telegram_general_topic_id: Option<String>,
19 pub discord_general_channel_id: Option<String>,
20 pub control_telegram_topic_id: Option<String>,
22 pub control_discord_channel_id: Option<String>,
24}
25
26#[derive(Debug)]
28pub struct ChannelRouter {
29 target_to_repo: HashMap<(String, String), String>,
31 repo_to_targets: HashMap<String, HashMap<String, String>>,
33 general: HashMap<String, String>,
35 control: HashMap<String, String>,
37 project_list: Vec<String>,
39}
40
41impl ChannelRouter {
42 pub fn new(global: &GlobalChannelConfig, projects: &[(String, ProjectChannelConfig)]) -> Self {
44 let mut target_to_repo = HashMap::new();
45 let mut repo_to_targets: HashMap<String, HashMap<String, String>> = HashMap::new();
46 let mut general = HashMap::new();
47 let mut control = HashMap::new();
48 let mut project_list = Vec::new();
49
50 if let Some(ref id) = global.telegram_general_topic_id {
51 general.insert("telegram".to_string(), id.clone());
52 }
53 if let Some(ref id) = global.discord_general_channel_id {
54 general.insert("discord".to_string(), id.clone());
55 }
56 if let Some(ref id) = global.control_telegram_topic_id {
57 control.insert("telegram".to_string(), id.clone());
58 }
59 if let Some(ref id) = global.control_discord_channel_id {
60 control.insert("discord".to_string(), id.clone());
61 }
62
63 for (repo, config) in projects {
64 project_list.push(repo.clone());
65 let mut targets = HashMap::new();
66
67 if let Some(ref topic_id) = config.telegram_topic_id {
68 target_to_repo.insert(("telegram".to_string(), topic_id.clone()), repo.clone());
69 targets.insert("telegram".to_string(), topic_id.clone());
70 }
71
72 if let Some(ref channel_id) = config.discord_channel_id {
73 target_to_repo.insert(("discord".to_string(), channel_id.clone()), repo.clone());
74 targets.insert("discord".to_string(), channel_id.clone());
75 }
76
77 repo_to_targets.insert(repo.clone(), targets);
78 }
79
80 Self {
81 target_to_repo,
82 repo_to_targets,
83 general,
84 control,
85 project_list,
86 }
87 }
88
89 pub fn resolve_project(&self, channel: &str, topic_or_channel_id: &str) -> Option<&str> {
92 self.target_to_repo
93 .get(&(channel.to_string(), topic_or_channel_id.to_string()))
94 .map(std::string::String::as_str)
95 }
96
97 pub fn is_general(&self, channel: &str, topic_or_channel_id: &str) -> bool {
99 self.general
100 .get(channel)
101 .is_some_and(|id| id == topic_or_channel_id)
102 }
103
104 pub fn is_control_channel(&self, channel: &str, topic_or_channel_id: &str) -> bool {
106 self.control
107 .get(channel)
108 .is_some_and(|id| id == topic_or_channel_id)
109 }
110
111 pub fn target_for_project(&self, repo: &str, channel: &str) -> Option<&str> {
113 self.repo_to_targets
114 .get(repo)
115 .and_then(|targets| targets.get(channel))
116 .map(std::string::String::as_str)
117 }
118
119 pub fn projects(&self) -> &[String] {
121 &self.project_list
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 fn test_router() -> ChannelRouter {
130 let global = GlobalChannelConfig {
131 telegram_general_topic_id: Some("1".to_string()),
132 discord_general_channel_id: Some("9999".to_string()),
133 ..Default::default()
134 };
135 let projects = vec![
136 (
137 "owner/orch".to_string(),
138 ProjectChannelConfig {
139 telegram_topic_id: Some("42".to_string()),
140 discord_channel_id: Some("1111".to_string()),
141 },
142 ),
143 (
144 "owner/bean".to_string(),
145 ProjectChannelConfig {
146 telegram_topic_id: Some("87".to_string()),
147 discord_channel_id: Some("2222".to_string()),
148 },
149 ),
150 ];
151 ChannelRouter::new(&global, &projects)
152 }
153
154 #[test]
155 fn resolve_telegram_topic_to_project() {
156 let router = test_router();
157 assert_eq!(router.resolve_project("telegram", "42"), Some("owner/orch"));
158 assert_eq!(router.resolve_project("telegram", "87"), Some("owner/bean"));
159 assert_eq!(router.resolve_project("telegram", "99"), None);
160 }
161
162 #[test]
163 fn resolve_discord_channel_to_project() {
164 let router = test_router();
165 assert_eq!(
166 router.resolve_project("discord", "1111"),
167 Some("owner/orch")
168 );
169 assert_eq!(
170 router.resolve_project("discord", "2222"),
171 Some("owner/bean")
172 );
173 }
174
175 #[test]
176 fn is_general_channel() {
177 let router = test_router();
178 assert!(router.is_general("telegram", "1"));
179 assert!(router.is_general("discord", "9999"));
180 assert!(!router.is_general("telegram", "42"));
181 assert!(!router.is_general("discord", "1111"));
182 }
183
184 #[test]
185 fn is_control_channel() {
186 let global = GlobalChannelConfig {
187 telegram_general_topic_id: Some("1".to_string()),
188 discord_general_channel_id: Some("9999".to_string()),
189 control_telegram_topic_id: Some("55".to_string()),
190 control_discord_channel_id: Some("77".to_string()),
191 };
192 let router = ChannelRouter::new(&global, &[]);
193 assert!(router.is_control_channel("telegram", "55"));
194 assert!(router.is_control_channel("discord", "77"));
195 assert!(!router.is_control_channel("telegram", "1")); assert!(!router.is_control_channel("discord", "9999")); assert!(!router.is_control_channel("telegram", "42")); assert!(!router.is_control_channel("slack", "55")); }
200
201 #[test]
202 fn control_channel_unconfigured_returns_false() {
203 let router = test_router(); assert!(!router.is_control_channel("telegram", "1"));
205 assert!(!router.is_control_channel("discord", "9999"));
206 }
207
208 #[test]
209 fn target_for_project() {
210 let router = test_router();
211 assert_eq!(
212 router.target_for_project("owner/orch", "telegram"),
213 Some("42")
214 );
215 assert_eq!(
216 router.target_for_project("owner/bean", "discord"),
217 Some("2222")
218 );
219 assert_eq!(router.target_for_project("owner/unknown", "telegram"), None);
220 }
221
222 #[test]
223 fn projects_list() {
224 let router = test_router();
225 assert_eq!(router.projects().len(), 2);
226 assert!(router.projects().contains(&"owner/orch".to_string()));
227 assert!(router.projects().contains(&"owner/bean".to_string()));
228 }
229
230 #[test]
231 fn unmapped_target_returns_none() {
232 let router = test_router();
233 assert_eq!(router.resolve_project("telegram", "1"), None); assert_eq!(router.resolve_project("slack", "anything"), None);
235 }
236
237 #[test]
238 fn project_with_custom_config_doesnt_affect_others() {
239 let global = GlobalChannelConfig {
240 telegram_general_topic_id: Some("1".into()),
241 discord_general_channel_id: None,
242 ..Default::default()
243 };
244 let projects = vec![
245 (
246 "owner/orch".into(),
247 ProjectChannelConfig {
248 telegram_topic_id: Some("42".into()),
249 ..Default::default()
250 },
251 ),
252 (
253 "owner/bean".into(),
254 ProjectChannelConfig {
255 telegram_topic_id: Some("87".into()),
256 ..Default::default()
257 },
258 ),
259 ];
260 let router = ChannelRouter::new(&global, &projects);
261 assert_eq!(router.resolve_project("telegram", "42"), Some("owner/orch"));
263 assert_eq!(router.resolve_project("telegram", "87"), Some("owner/bean"));
264 assert_eq!(
266 router.target_for_project("owner/orch", "telegram"),
267 Some("42")
268 );
269 assert_eq!(
270 router.target_for_project("owner/bean", "telegram"),
271 Some("87")
272 );
273 assert!(router.is_general("telegram", "1"));
275 assert_eq!(router.projects().len(), 2);
277 }
278}