Skip to main content

gsm_core/
default_packs.rs

1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::path_safety::normalize_under_root;
6
7/// Configuration controlling which default messaging adapter packs to load.
8#[derive(Debug, Clone, PartialEq, Eq, Default)]
9pub struct DefaultAdapterPacksConfig {
10    /// Load all default packs shipped in `packs/messaging`.
11    pub install_all: bool,
12    /// Specific default pack ids to load (e.g., `["teams", "slack"]`).
13    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/// Pack metadata mapped to its file path.
26#[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/// Resolved pack path and raw contents.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct LoadedPack {
66    pub id: String,
67    pub path: PathBuf,
68    pub raw: String,
69}
70
71/// Resolve pack file system paths without reading contents.
72pub 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
82/// Additional adapter pack paths from an explicit list.
83pub 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
97/// Load and parse default adapter packs into an adapter registry.
98pub 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
107/// Resolve which default adapter packs should be loaded based on config.
108pub 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
128/// Load the default adapter pack files from a root directory.
129///
130/// `packs_root` should point at the directory containing `messaging/`.
131pub 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}