1use serde::{Deserialize, Serialize};
52use thiserror::Error;
53
54#[derive(Debug, Error)]
56pub enum PackageError {
57 #[error("serialization error: {0}")]
59 Serialization(#[from] serde_json::Error),
60
61 #[error("package not found: {0}")]
63 NotFound(String),
64
65 #[error("package already installed: {0}")]
67 AlreadyInstalled(String),
68
69 #[error("invalid package: {0}")]
71 Invalid(String),
72
73 #[error("component does not support packages")]
75 NotSupported,
76
77 #[error("install failed: {0}")]
79 InstallFailed(String),
80
81 #[error("uninstall failed: {0}")]
83 UninstallFailed(String),
84}
85
86pub const PACKAGE_VERSION: u32 = 1;
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93pub struct PackageInfo {
94 pub id: String,
96
97 pub name: String,
99
100 pub version: String,
102
103 pub description: String,
105
106 pub enabled: bool,
108}
109
110impl PackageInfo {
111 #[must_use]
113 pub fn new(
114 id: impl Into<String>,
115 name: impl Into<String>,
116 version: impl Into<String>,
117 description: impl Into<String>,
118 ) -> Self {
119 Self {
120 id: id.into(),
121 name: name.into(),
122 version: version.into(),
123 description: description.into(),
124 enabled: true,
125 }
126 }
127
128 #[must_use]
130 pub fn with_enabled(mut self, enabled: bool) -> Self {
131 self.enabled = enabled;
132 self
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Package {
141 pub info: PackageInfo,
143
144 pub version: u32,
146
147 pub content: serde_json::Value,
149}
150
151impl Package {
152 pub fn new<T: Serialize>(info: PackageInfo, content: &T) -> Result<Self, PackageError> {
163 Ok(Self {
164 info,
165 version: PACKAGE_VERSION,
166 content: serde_json::to_value(content)?,
167 })
168 }
169
170 #[must_use]
172 pub fn from_value(info: PackageInfo, content: serde_json::Value) -> Self {
173 Self {
174 info,
175 version: PACKAGE_VERSION,
176 content,
177 }
178 }
179
180 pub fn to_content<T: for<'de> Deserialize<'de>>(&self) -> Result<T, PackageError> {
186 Ok(serde_json::from_value(self.content.clone())?)
187 }
188
189 #[must_use]
191 pub fn id(&self) -> &str {
192 &self.info.id
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
198pub enum PackageSupport {
199 Enabled,
201
202 #[default]
204 Disabled,
205}
206
207pub trait Packageable {
218 fn list_packages(&self) -> &[PackageInfo];
220
221 fn install_package(&mut self, package: &Package) -> Result<(), PackageError>;
227
228 fn uninstall_package(&mut self, package_id: &str) -> Result<(), PackageError>;
234
235 fn is_installed(&self, package_id: &str) -> bool {
237 self.list_packages().iter().any(|p| p.id == package_id)
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
246 struct TestConfig {
247 value: String,
248 }
249
250 #[test]
251 fn package_info_new() {
252 let info = PackageInfo::new("test-pkg", "Test Package", "1.0.0", "A test package");
253 assert_eq!(info.id, "test-pkg");
254 assert_eq!(info.name, "Test Package");
255 assert_eq!(info.version, "1.0.0");
256 assert!(info.enabled);
257 }
258
259 #[test]
260 fn package_info_with_enabled() {
261 let info = PackageInfo::new("test-pkg", "Test", "1.0.0", "Test").with_enabled(false);
262 assert!(!info.enabled);
263 }
264
265 #[test]
266 fn package_roundtrip() {
267 let info = PackageInfo::new("test-pkg", "Test", "1.0.0", "Test");
268 let config = TestConfig {
269 value: "hello".into(),
270 };
271
272 let package = Package::new(info, &config).expect("create package");
273 let restored: TestConfig = package.to_content().expect("deserialize content");
274
275 assert_eq!(config, restored);
276 }
277
278 #[test]
279 fn package_from_value() {
280 let info = PackageInfo::new("test-pkg", "Test", "1.0.0", "Test");
281 let content = serde_json::json!({"key": "value"});
282
283 let package = Package::from_value(info, content.clone());
284 assert_eq!(package.content, content);
285 }
286
287 #[test]
288 fn package_id() {
289 let info = PackageInfo::new("my-package", "My Package", "1.0.0", "Test");
290 let package = Package::from_value(info, serde_json::Value::Null);
291 assert_eq!(package.id(), "my-package");
292 }
293
294 struct TestComponent {
295 packages: Vec<PackageInfo>,
296 config_value: String,
297 }
298
299 impl Packageable for TestComponent {
300 fn list_packages(&self) -> &[PackageInfo] {
301 &self.packages
302 }
303
304 fn install_package(&mut self, package: &Package) -> Result<(), PackageError> {
305 if self.is_installed(package.id()) {
306 return Err(PackageError::AlreadyInstalled(package.id().to_string()));
307 }
308 let config: TestConfig = package.to_content()?;
309 self.config_value = config.value;
310 self.packages.push(package.info.clone());
311 Ok(())
312 }
313
314 fn uninstall_package(&mut self, package_id: &str) -> Result<(), PackageError> {
315 if !self.is_installed(package_id) {
316 return Err(PackageError::NotFound(package_id.to_string()));
317 }
318 self.packages.retain(|p| p.id != package_id);
319 self.config_value = String::new();
320 Ok(())
321 }
322 }
323
324 #[test]
325 fn packageable_install_uninstall() {
326 let mut comp = TestComponent {
327 packages: vec![],
328 config_value: String::new(),
329 };
330
331 let info = PackageInfo::new("decorator", "Decorator", "1.0.0", "Add decoration");
332 let config = TestConfig {
333 value: "decorated".into(),
334 };
335 let package =
336 Package::new(info, &config).expect("Package::new should create a valid package");
337
338 comp.install_package(&package)
340 .expect("first install of 'decorator' package should succeed");
341 assert!(comp.is_installed("decorator"));
342 assert_eq!(comp.config_value, "decorated");
343 assert_eq!(comp.list_packages().len(), 1);
344
345 comp.uninstall_package("decorator")
347 .expect("uninstall of installed 'decorator' package should succeed");
348 assert!(!comp.is_installed("decorator"));
349 assert_eq!(comp.config_value, "");
350 assert!(comp.list_packages().is_empty());
351 }
352
353 #[test]
354 fn packageable_already_installed() {
355 let mut comp = TestComponent {
356 packages: vec![],
357 config_value: String::new(),
358 };
359
360 let info = PackageInfo::new("test", "Test", "1.0.0", "Test");
361 let config = TestConfig { value: "a".into() };
362 let package =
363 Package::new(info, &config).expect("Package::new should create a valid test package");
364
365 comp.install_package(&package)
366 .expect("first install of 'test' package should succeed");
367 let result = comp.install_package(&package);
368 assert!(matches!(result, Err(PackageError::AlreadyInstalled(_))));
369 }
370
371 #[test]
372 fn packageable_not_found() {
373 let mut comp = TestComponent {
374 packages: vec![],
375 config_value: String::new(),
376 };
377
378 let result = comp.uninstall_package("nonexistent");
379 assert!(matches!(result, Err(PackageError::NotFound(_))));
380 }
381}