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