gsm_core/
default_packs.rs1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::path_safety::normalize_under_root;
6
7#[derive(Debug, Clone, PartialEq, Eq, Default)]
9pub struct DefaultAdapterPacksConfig {
10 pub install_all: bool,
12 pub selected: Vec<String>,
14}
15
16impl DefaultAdapterPacksConfig {
17 pub fn from_settings(install_all: bool, selected: Vec<String>) -> Self {
18 Self {
19 install_all,
20 selected,
21 }
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct DefaultAdapterPack {
28 pub id: &'static str,
29 pub filename: &'static str,
30}
31
32const DEFAULT_PACKS: &[DefaultAdapterPack] = &[
33 DefaultAdapterPack {
34 id: "slack",
35 filename: "slack.yaml",
36 },
37 DefaultAdapterPack {
38 id: "teams",
39 filename: "teams.yaml",
40 },
41 DefaultAdapterPack {
42 id: "webex",
43 filename: "webex.yaml",
44 },
45 DefaultAdapterPack {
46 id: "webchat",
47 filename: "webchat.yaml",
48 },
49 DefaultAdapterPack {
50 id: "whatsapp",
51 filename: "whatsapp.yaml",
52 },
53 DefaultAdapterPack {
54 id: "telegram",
55 filename: "telegram.yaml",
56 },
57 DefaultAdapterPack {
58 id: "local",
59 filename: "local.yaml",
60 },
61];
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct LoadedPack {
66 pub id: String,
67 pub path: PathBuf,
68 pub raw: String,
69}
70
71pub fn default_adapter_pack_paths(
73 packs_root: &Path,
74 config: &DefaultAdapterPacksConfig,
75) -> Vec<PathBuf> {
76 resolve_default_adapter_packs(config)
77 .into_iter()
78 .map(|pack| packs_root.join("messaging").join(pack.filename))
79 .collect()
80}
81
82pub fn adapter_pack_paths_from_list(paths: &[String]) -> Vec<PathBuf> {
84 paths
85 .iter()
86 .filter_map(|raw| {
87 let trimmed = raw.trim();
88 if trimmed.is_empty() {
89 None
90 } else {
91 Some(PathBuf::from(trimmed))
92 }
93 })
94 .collect()
95}
96
97pub fn load_default_adapter_registry(
99 packs_root: &Path,
100 config: &DefaultAdapterPacksConfig,
101) -> anyhow::Result<crate::AdapterRegistry> {
102 let paths = default_adapter_pack_paths(packs_root, config);
103 crate::adapter_registry::load_adapters_from_pack_files(packs_root, &paths)
104 .map_err(|err| err.context("failed to load default messaging adapter packs"))
105}
106
107pub fn resolve_default_adapter_packs(
109 config: &DefaultAdapterPacksConfig,
110) -> Vec<&'static DefaultAdapterPack> {
111 if config.install_all {
112 return DEFAULT_PACKS.iter().collect();
113 }
114 if config.selected.is_empty() {
115 return Vec::new();
116 }
117 let selected: HashSet<String> = config
118 .selected
119 .iter()
120 .map(|s| s.to_ascii_lowercase())
121 .collect();
122 DEFAULT_PACKS
123 .iter()
124 .filter(|pack| selected.contains(pack.id))
125 .collect()
126}
127
128pub fn load_default_adapter_packs_from(
132 packs_root: &Path,
133 config: &DefaultAdapterPacksConfig,
134) -> std::io::Result<Vec<LoadedPack>> {
135 let resolved = resolve_default_adapter_packs(config);
136 let mut out = Vec::with_capacity(resolved.len());
137 for pack in resolved {
138 let relative = Path::new("messaging").join(pack.filename);
139 let safe = normalize_under_root(packs_root, &relative).map_err(std::io::Error::other)?;
140 let raw = fs::read_to_string(&safe)?;
141 out.push(LoadedPack {
142 id: pack.id.to_string(),
143 path: safe,
144 raw,
145 });
146 }
147 Ok(out)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::path::PathBuf;
154
155 #[test]
156 fn adapter_pack_paths_list_filters_empty() {
157 let paths = adapter_pack_paths_from_list(&[
158 "/tmp/one.yaml".into(),
159 " /tmp/two.yaml ".into(),
160 "".into(),
161 ]);
162 assert_eq!(
163 paths,
164 vec![
165 PathBuf::from("/tmp/one.yaml"),
166 PathBuf::from("/tmp/two.yaml")
167 ]
168 );
169 }
170
171 #[test]
172 fn resolve_all_when_flag_set() {
173 let cfg = DefaultAdapterPacksConfig {
174 install_all: true,
175 selected: vec![],
176 };
177 let resolved = resolve_default_adapter_packs(&cfg);
178 assert_eq!(resolved.len(), DEFAULT_PACKS.len());
179 }
180
181 #[test]
182 fn resolve_subset_when_listed() {
183 let cfg = DefaultAdapterPacksConfig {
184 install_all: false,
185 selected: vec!["teams".into(), "slack".into(), "missing".into()],
186 };
187 let resolved = resolve_default_adapter_packs(&cfg);
188 let ids: HashSet<&str> = resolved.iter().map(|p| p.id).collect();
189 assert_eq!(ids, HashSet::from(["teams", "slack"]));
190 }
191
192 #[test]
193 fn resolve_empty_when_no_selection() {
194 let cfg = DefaultAdapterPacksConfig {
195 install_all: false,
196 selected: Vec::new(),
197 };
198 let resolved = resolve_default_adapter_packs(&cfg);
199 assert!(resolved.is_empty());
200 }
201
202 #[test]
203 fn resolve_all_when_install_all_true() {
204 let cfg = DefaultAdapterPacksConfig {
205 install_all: true,
206 selected: Vec::new(),
207 };
208 let resolved = resolve_default_adapter_packs(&cfg);
209 assert_eq!(resolved.len(), super::DEFAULT_PACKS.len());
210 }
211
212 #[test]
213 fn resolve_exact_subset() {
214 let cfg = DefaultAdapterPacksConfig {
215 install_all: false,
216 selected: vec!["slack".into(), "telegram".into()],
217 };
218 let resolved = resolve_default_adapter_packs(&cfg);
219 let ids: Vec<_> = resolved.iter().map(|p| p.id).collect();
220 assert_eq!(ids, vec!["slack", "telegram"]);
221 }
222}