1use zeroclaw_config::schema::ChannelsConfig;
9use zeroclaw_config::traits::ChannelInfo;
10
11struct ChannelCompileSpec {
12 schema_name: Option<&'static str>,
16 type_keys: &'static [&'static str],
19 compiled: bool,
20}
21
22const CHANNEL_COMPILE_SPECS: &[ChannelCompileSpec] = &[
26 ChannelCompileSpec {
27 schema_name: Some("Telegram"),
28 type_keys: &["telegram"],
29 compiled: cfg!(feature = "channel-telegram"),
30 },
31 ChannelCompileSpec {
32 schema_name: Some("Discord"),
33 type_keys: &["discord"],
34 compiled: cfg!(feature = "channel-discord"),
35 },
36 ChannelCompileSpec {
37 schema_name: Some("Slack"),
38 type_keys: &["slack"],
39 compiled: cfg!(feature = "channel-slack"),
40 },
41 ChannelCompileSpec {
42 schema_name: Some("Mattermost"),
43 type_keys: &["mattermost"],
44 compiled: cfg!(feature = "channel-mattermost"),
45 },
46 ChannelCompileSpec {
47 schema_name: Some("iMessage"),
48 type_keys: &["imessage"],
49 compiled: cfg!(feature = "channel-imessage"),
50 },
51 ChannelCompileSpec {
52 schema_name: Some("Matrix"),
53 type_keys: &["matrix"],
54 compiled: cfg!(feature = "channel-matrix"),
55 },
56 ChannelCompileSpec {
57 schema_name: Some("Signal"),
58 type_keys: &["signal"],
59 compiled: cfg!(feature = "channel-signal"),
60 },
61 ChannelCompileSpec {
62 schema_name: Some("WhatsApp"),
63 type_keys: &["whatsapp"],
64 compiled: cfg!(feature = "channel-whatsapp-cloud"),
65 },
66 ChannelCompileSpec {
67 schema_name: Some("WhatsApp Web"),
68 type_keys: &["whatsapp-web", "whatsapp_web"],
69 compiled: cfg!(feature = "whatsapp-web"),
70 },
71 ChannelCompileSpec {
72 schema_name: Some("Linq"),
73 type_keys: &["linq"],
74 compiled: cfg!(feature = "channel-linq"),
75 },
76 ChannelCompileSpec {
77 schema_name: Some("WATI"),
78 type_keys: &["wati"],
79 compiled: cfg!(feature = "channel-wati"),
80 },
81 ChannelCompileSpec {
82 schema_name: Some("NextCloud Talk"),
83 type_keys: &["nextcloud-talk", "nextcloud_talk"],
84 compiled: cfg!(feature = "channel-nextcloud"),
85 },
86 ChannelCompileSpec {
87 schema_name: Some("Email"),
88 type_keys: &["email"],
89 compiled: cfg!(feature = "channel-email"),
90 },
91 ChannelCompileSpec {
92 schema_name: Some("Gmail Push"),
93 type_keys: &["gmail-push", "gmail_push"],
94 compiled: cfg!(feature = "channel-email"),
95 },
96 ChannelCompileSpec {
97 schema_name: Some("IRC"),
98 type_keys: &["irc"],
99 compiled: cfg!(feature = "channel-irc"),
100 },
101 ChannelCompileSpec {
102 schema_name: Some("Twitch"),
103 type_keys: &["twitch"],
104 compiled: cfg!(feature = "channel-twitch"),
105 },
106 ChannelCompileSpec {
107 schema_name: Some("Lark"),
108 type_keys: &["lark", "feishu"],
109 compiled: cfg!(feature = "channel-lark"),
110 },
111 ChannelCompileSpec {
112 schema_name: Some("DingTalk"),
113 type_keys: &["dingtalk"],
114 compiled: cfg!(feature = "channel-dingtalk"),
115 },
116 ChannelCompileSpec {
117 schema_name: Some("WeCom"),
118 type_keys: &["wecom"],
119 compiled: cfg!(feature = "channel-wecom"),
120 },
121 ChannelCompileSpec {
122 schema_name: Some("WeCom WebSocket"),
123 type_keys: &["wecom-ws", "wecom_ws"],
124 compiled: cfg!(feature = "channel-wecom-ws"),
125 },
126 ChannelCompileSpec {
127 schema_name: Some("WeChat"),
128 type_keys: &["wechat"],
129 compiled: cfg!(feature = "channel-wechat"),
130 },
131 ChannelCompileSpec {
132 schema_name: Some("QQ Official"),
133 type_keys: &["qq"],
134 compiled: cfg!(feature = "channel-qq"),
135 },
136 ChannelCompileSpec {
137 schema_name: Some("Nostr"),
138 type_keys: &["nostr"],
139 compiled: cfg!(feature = "channel-nostr"),
140 },
141 ChannelCompileSpec {
142 schema_name: Some("ClawdTalk"),
143 type_keys: &["clawdtalk"],
144 compiled: cfg!(feature = "channel-clawdtalk"),
145 },
146 ChannelCompileSpec {
147 schema_name: Some("Reddit"),
148 type_keys: &["reddit"],
149 compiled: cfg!(feature = "channel-reddit"),
150 },
151 ChannelCompileSpec {
152 schema_name: Some("Bluesky"),
153 type_keys: &["bluesky"],
154 compiled: cfg!(feature = "channel-bluesky"),
155 },
156 ChannelCompileSpec {
157 schema_name: Some("X/Twitter"),
158 type_keys: &["twitter"],
159 compiled: cfg!(feature = "channel-twitter"),
160 },
161 ChannelCompileSpec {
162 schema_name: Some("Mochat"),
163 type_keys: &["mochat"],
164 compiled: cfg!(feature = "channel-mochat"),
165 },
166 ChannelCompileSpec {
167 schema_name: Some("LINE"),
168 type_keys: &["line"],
169 compiled: cfg!(feature = "channel-line"),
170 },
171 ChannelCompileSpec {
172 schema_name: Some("Voice Call"),
173 type_keys: &["voice-call", "voice_call"],
174 compiled: cfg!(feature = "channel-voice-call"),
175 },
176 ChannelCompileSpec {
177 schema_name: Some("VoiceWake"),
178 type_keys: &["voice-wake", "voice_wake"],
179 compiled: cfg!(feature = "voice-wake"),
180 },
181 ChannelCompileSpec {
182 schema_name: Some("MQTT"),
183 type_keys: &["mqtt"],
184 compiled: cfg!(feature = "channel-mqtt"),
185 },
186 ChannelCompileSpec {
187 schema_name: Some("AMQP"),
188 type_keys: &["amqp"],
189 compiled: cfg!(feature = "channel-amqp"),
190 },
191 ChannelCompileSpec {
192 schema_name: Some("Webhook"),
193 type_keys: &["webhook"],
194 compiled: cfg!(feature = "channel-webhook"),
195 },
196 ChannelCompileSpec {
197 schema_name: None,
198 type_keys: &["acp-server", "acp_server"],
199 compiled: cfg!(feature = "channel-acp-server"),
200 },
201];
202
203fn compiled_channel_names() -> impl Iterator<Item = &'static str> {
204 CHANNEL_COMPILE_SPECS
205 .iter()
206 .filter(|spec| spec.compiled)
207 .filter_map(|spec| spec.schema_name)
208}
209
210pub fn compiled_channels(cfg: &ChannelsConfig) -> Vec<ChannelInfo> {
217 cfg.channels()
218 .into_iter()
219 .filter(|info| compiled_channel_names().any(|name| name == info.name))
220 .collect()
221}
222
223pub fn is_channel_type_compiled(channel_type: &str) -> bool {
228 for spec in CHANNEL_COMPILE_SPECS {
229 if spec.type_keys.contains(&channel_type) {
230 return spec.compiled;
231 }
232 }
233 false
234}
235
236#[cfg(test)]
237mod tests {
238 use super::{CHANNEL_COMPILE_SPECS, ChannelsConfig};
239 use super::{compiled_channels, is_channel_type_compiled};
240 use std::collections::BTreeSet;
241
242 #[cfg(feature = "default-channels")]
243 #[test]
244 fn channel_type_compilation_tracks_enabled_features() {
245 assert!(is_channel_type_compiled("telegram"));
246 assert!(is_channel_type_compiled("email"));
247 assert!(is_channel_type_compiled("webhook"));
248 assert!(is_channel_type_compiled("acp-server"));
249 assert_eq!(
250 is_channel_type_compiled("nextcloud-talk"),
251 cfg!(feature = "channel-nextcloud")
252 );
253 assert_eq!(
254 is_channel_type_compiled("linq"),
255 cfg!(feature = "channel-linq")
256 );
257 }
258
259 #[test]
260 fn compiled_channel_names_are_schema_names() {
261 let cfg = ChannelsConfig::default();
262 let schema_names: BTreeSet<_> = cfg.channels().into_iter().map(|info| info.name).collect();
263
264 for name in CHANNEL_COMPILE_SPECS
265 .iter()
266 .filter_map(|spec| spec.schema_name)
267 {
268 assert!(
269 schema_names.contains(name),
270 "compiled channel name `{name}` is missing from ChannelsConfig::channels()"
271 );
272 }
273 }
274
275 #[test]
276 fn compiled_channels_match_expected_schema_names() {
277 let cfg = ChannelsConfig::default();
278 let actual: BTreeSet<_> = compiled_channels(&cfg)
279 .into_iter()
280 .map(|info| info.name)
281 .collect();
282 let expected: BTreeSet<_> = CHANNEL_COMPILE_SPECS
283 .iter()
284 .filter(|spec| spec.compiled)
285 .filter_map(|spec| spec.schema_name)
286 .collect();
287
288 assert_eq!(actual, expected);
289 }
290
291 #[test]
292 fn channel_type_compilation_matches_inventory_specs() {
293 for spec in CHANNEL_COMPILE_SPECS {
294 for key in spec.type_keys {
295 assert_eq!(
296 is_channel_type_compiled(key),
297 spec.compiled,
298 "channel type key `{key}` drifted from its compile spec"
299 );
300 }
301 }
302
303 assert!(!is_channel_type_compiled("not-a-channel"));
304 }
305
306 #[test]
307 fn channel_compile_specs_do_not_duplicate_entries() {
308 let mut seen_names = BTreeSet::new();
309 let mut seen_keys = BTreeSet::new();
310
311 for spec in CHANNEL_COMPILE_SPECS {
312 if let Some(name) = spec.schema_name {
313 assert!(
314 seen_names.insert(name),
315 "compiled channel name `{name}` appears more than once"
316 );
317 }
318
319 for key in spec.type_keys {
320 assert!(
321 seen_keys.insert(*key),
322 "compiled channel type key `{key}` appears more than once"
323 );
324 }
325 }
326 }
327
328 #[test]
329 fn channel_compile_specs_cover_schema_channel_types() {
330 let out_of_scope = BTreeSet::from([
331 "voice_duplex",
334 ]);
335
336 for channel_type in zeroclaw_config::schema::v2::V3_CHANNEL_TYPES {
337 if out_of_scope.contains(channel_type) {
338 continue;
339 }
340 let kebab = channel_type.replace('_', "-");
341 assert!(
342 CHANNEL_COMPILE_SPECS.iter().any(|spec| spec
343 .type_keys
344 .iter()
345 .any(|key| *key == *channel_type || *key == kebab)),
346 "schema channel type `{channel_type}` is missing from CHANNEL_COMPILE_SPECS"
347 );
348 }
349 }
350}