lean_ctx/core/addons/
store.rs1use std::collections::BTreeMap;
9use std::path::PathBuf;
10
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct InstalledAddon {
16 pub name: String,
17 pub version: String,
18 pub source: String,
20 pub gateway_server: String,
22}
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct InstalledStore {
27 #[serde(default)]
28 pub addons: BTreeMap<String, InstalledAddon>,
29}
30
31fn store_path() -> Result<PathBuf, String> {
32 Ok(crate::core::data_dir::lean_ctx_data_dir()?
33 .join("addons")
34 .join("installed.json"))
35}
36
37impl InstalledStore {
38 pub fn load() -> Self {
40 let Ok(path) = store_path() else {
41 return Self::default();
42 };
43 match std::fs::read_to_string(&path) {
44 Ok(raw) if !raw.trim().is_empty() => serde_json::from_str(&raw).unwrap_or_default(),
45 _ => Self::default(),
46 }
47 }
48
49 pub fn save(&self) -> Result<(), String> {
51 let path = store_path()?;
52 if let Some(parent) = path.parent() {
53 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
54 }
55 let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
56 std::fs::write(&path, json).map_err(|e| e.to_string())
57 }
58
59 pub fn get(&self, name: &str) -> Option<&InstalledAddon> {
60 self.addons.get(name)
61 }
62
63 pub fn list(&self) -> Vec<&InstalledAddon> {
65 self.addons.values().collect()
66 }
67
68 pub fn upsert(&mut self, addon: InstalledAddon) {
69 self.addons.insert(addon.name.clone(), addon);
70 }
71
72 pub fn remove(&mut self, name: &str) -> Option<InstalledAddon> {
73 self.addons.remove(name)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::core::data_dir::isolated_data_dir;
81
82 fn sample(name: &str) -> InstalledAddon {
83 InstalledAddon {
84 name: name.to_string(),
85 version: "1.0.0".into(),
86 source: "registry".into(),
87 gateway_server: name.to_string(),
88 }
89 }
90
91 #[test]
92 fn round_trips_through_disk() {
93 let _data = isolated_data_dir();
94 assert!(InstalledStore::load().list().is_empty());
95
96 let mut store = InstalledStore::default();
97 store.upsert(sample("alpha"));
98 store.upsert(sample("beta"));
99 store.save().expect("save");
100
101 let reloaded = InstalledStore::load();
102 assert_eq!(reloaded.list().len(), 2);
103 assert!(reloaded.get("alpha").is_some());
104
105 let mut reloaded = reloaded;
106 assert!(reloaded.remove("alpha").is_some());
107 reloaded.save().expect("save");
108 assert!(InstalledStore::load().get("alpha").is_none());
109 assert!(InstalledStore::load().get("beta").is_some());
110 }
111}