gsm_core/
default_packs.rs1use std::collections::HashSet;
2use std::env;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct DefaultAdapterPacksConfig {
9 pub install_all: bool,
11 pub selected: Vec<String>,
13}
14
15impl DefaultAdapterPacksConfig {
16 pub fn from_env() -> Self {
17 let install_all = env::var("MESSAGING_INSTALL_ALL_DEFAULT_ADAPTER_PACKS")
18 .map(|v| v.eq_ignore_ascii_case("true"))
19 .unwrap_or(false);
20 let selected = env::var("MESSAGING_DEFAULT_ADAPTER_PACKS")
21 .ok()
22 .map(|raw| {
23 raw.split(',')
24 .filter_map(|s| {
25 let trimmed = s.trim();
26 if trimmed.is_empty() {
27 None
28 } else {
29 Some(trimmed.to_string())
30 }
31 })
32 .collect()
33 })
34 .unwrap_or_default();
35 Self {
36 install_all,
37 selected,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct DefaultAdapterPack {
45 pub id: &'static str,
46 pub filename: &'static str,
47}
48
49const DEFAULT_PACKS: &[DefaultAdapterPack] = &[
50 DefaultAdapterPack {
51 id: "slack",
52 filename: "slack.yaml",
53 },
54 DefaultAdapterPack {
55 id: "teams",
56 filename: "teams.yaml",
57 },
58 DefaultAdapterPack {
59 id: "webex",
60 filename: "webex.yaml",
61 },
62 DefaultAdapterPack {
63 id: "webchat",
64 filename: "webchat.yaml",
65 },
66 DefaultAdapterPack {
67 id: "whatsapp",
68 filename: "whatsapp.yaml",
69 },
70 DefaultAdapterPack {
71 id: "telegram",
72 filename: "telegram.yaml",
73 },
74 DefaultAdapterPack {
75 id: "local",
76 filename: "local.yaml",
77 },
78];
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct LoadedPack {
83 pub id: String,
84 pub path: PathBuf,
85 pub raw: String,
86}
87
88pub fn default_adapter_pack_paths(
90 packs_root: &Path,
91 config: &DefaultAdapterPacksConfig,
92) -> Vec<PathBuf> {
93 resolve_default_adapter_packs(config)
94 .into_iter()
95 .map(|pack| packs_root.join("messaging").join(pack.filename))
96 .collect()
97}
98
99pub fn adapter_pack_paths_from_env() -> Vec<PathBuf> {
101 env::var("MESSAGING_ADAPTER_PACK_PATHS")
102 .ok()
103 .map(|raw| {
104 raw.split(',')
105 .filter_map(|s| {
106 let trimmed = s.trim();
107 if trimmed.is_empty() {
108 None
109 } else {
110 Some(PathBuf::from(trimmed))
111 }
112 })
113 .collect()
114 })
115 .unwrap_or_default()
116}
117
118pub fn load_default_adapter_registry(
120 packs_root: &Path,
121 config: &DefaultAdapterPacksConfig,
122) -> anyhow::Result<crate::AdapterRegistry> {
123 let paths = default_adapter_pack_paths(packs_root, config);
124 crate::adapter_registry::load_adapters_from_pack_files(&paths)
125 .map_err(|err| err.context("failed to load default messaging adapter packs"))
126}
127
128pub fn resolve_default_adapter_packs(
130 config: &DefaultAdapterPacksConfig,
131) -> Vec<&'static DefaultAdapterPack> {
132 if config.install_all {
133 return DEFAULT_PACKS.iter().collect();
134 }
135 if config.selected.is_empty() {
136 return Vec::new();
137 }
138 let selected: HashSet<String> = config
139 .selected
140 .iter()
141 .map(|s| s.to_ascii_lowercase())
142 .collect();
143 DEFAULT_PACKS
144 .iter()
145 .filter(|pack| selected.contains(pack.id))
146 .collect()
147}
148
149pub fn load_default_adapter_packs_from(
153 packs_root: &Path,
154 config: &DefaultAdapterPacksConfig,
155) -> std::io::Result<Vec<LoadedPack>> {
156 let resolved = resolve_default_adapter_packs(config);
157 let mut out = Vec::with_capacity(resolved.len());
158 for pack in resolved {
159 let path = packs_root.join("messaging").join(pack.filename);
160 let raw = fs::read_to_string(&path)?;
161 out.push(LoadedPack {
162 id: pack.id.to_string(),
163 path,
164 raw,
165 });
166 }
167 Ok(out)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use once_cell::sync::Lazy;
174 use std::path::PathBuf;
175 use std::sync::Mutex;
176
177 static ENV_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
178
179 struct GuardEnv;
180 impl GuardEnv {
181 fn new() -> Self {
182 Self
183 }
184 }
185 impl Drop for GuardEnv {
186 fn drop(&mut self) {
187 unsafe {
188 env::remove_var("MESSAGING_ADAPTER_PACK_PATHS");
189 env::remove_var("MESSAGING_INSTALL_ALL_DEFAULT_ADAPTER_PACKS");
190 env::remove_var("MESSAGING_DEFAULT_ADAPTER_PACKS");
191 }
192 }
193 }
194
195 #[test]
196 fn adapter_pack_paths_env_parses_list() {
197 let _lock = ENV_GUARD.lock().unwrap();
198 let _guard = GuardEnv::new();
199 unsafe {
200 env::set_var(
201 "MESSAGING_ADAPTER_PACK_PATHS",
202 "/tmp/one.yaml, /tmp/two.yaml ,",
203 );
204 }
205 let paths = adapter_pack_paths_from_env();
206 assert_eq!(
207 paths,
208 vec![
209 PathBuf::from("/tmp/one.yaml"),
210 PathBuf::from("/tmp/two.yaml")
211 ]
212 );
213 }
214
215 #[test]
216 fn config_parses_env() {
217 let _lock = ENV_GUARD.lock().unwrap();
218 let _guard = GuardEnv::new();
219 unsafe {
220 env::set_var("MESSAGING_INSTALL_ALL_DEFAULT_ADAPTER_PACKS", "true");
221 env::set_var("MESSAGING_DEFAULT_ADAPTER_PACKS", "teams, slack");
222 }
223 let cfg = DefaultAdapterPacksConfig::from_env();
224 assert!(cfg.install_all);
225 assert_eq!(cfg.selected, vec!["teams".to_string(), "slack".to_string()]);
226 }
227
228 #[test]
229 fn resolve_all_when_flag_set() {
230 let cfg = DefaultAdapterPacksConfig {
231 install_all: true,
232 selected: vec![],
233 };
234 let resolved = resolve_default_adapter_packs(&cfg);
235 assert_eq!(resolved.len(), DEFAULT_PACKS.len());
236 }
237
238 #[test]
239 fn resolve_subset_when_listed() {
240 let cfg = DefaultAdapterPacksConfig {
241 install_all: false,
242 selected: vec!["teams".into(), "slack".into(), "missing".into()],
243 };
244 let resolved = resolve_default_adapter_packs(&cfg);
245 let ids: HashSet<&str> = resolved.iter().map(|p| p.id).collect();
246 assert_eq!(ids, HashSet::from(["teams", "slack"]));
247 }
248
249 #[test]
250 fn resolve_empty_when_no_selection() {
251 let cfg = DefaultAdapterPacksConfig {
252 install_all: false,
253 selected: Vec::new(),
254 };
255 let resolved = resolve_default_adapter_packs(&cfg);
256 assert!(resolved.is_empty());
257 }
258
259 #[test]
260 fn resolve_all_when_install_all_true() {
261 let cfg = DefaultAdapterPacksConfig {
262 install_all: true,
263 selected: Vec::new(),
264 };
265 let resolved = resolve_default_adapter_packs(&cfg);
266 assert_eq!(resolved.len(), super::DEFAULT_PACKS.len());
267 }
268
269 #[test]
270 fn resolve_exact_subset() {
271 let cfg = DefaultAdapterPacksConfig {
272 install_all: false,
273 selected: vec!["slack".into(), "telegram".into()],
274 };
275 let resolved = resolve_default_adapter_packs(&cfg);
276 let ids: Vec<_> = resolved.iter().map(|p| p.id).collect();
277 assert_eq!(ids, vec!["slack", "telegram"]);
278 }
279}