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