Skip to main content

orcs_component/
package.rs

1//! Package support for component customization.
2//!
3//! The [`Packageable`] trait enables components to install and uninstall
4//! packages that modify their behavior.
5//!
6//! # Design
7//!
8//! Packageable is an **optional** trait. Components that don't implement it
9//! cannot have packages installed.
10//!
11//! # Example
12//!
13//! ```
14//! use orcs_component::{Packageable, Package, PackageInfo, PackageError};
15//! use serde::{Serialize, Deserialize};
16//!
17//! #[derive(Serialize, Deserialize)]
18//! struct DecoratorConfig {
19//!     prefix: String,
20//!     suffix: String,
21//! }
22//!
23//! struct EchoComponent {
24//!     prefix: String,
25//!     suffix: String,
26//!     installed_packages: Vec<PackageInfo>,
27//! }
28//!
29//! impl Packageable for EchoComponent {
30//!     fn list_packages(&self) -> &[PackageInfo] {
31//!         &self.installed_packages
32//!     }
33//!
34//!     fn install_package(&mut self, package: &Package) -> Result<(), PackageError> {
35//!         let config: DecoratorConfig = package.to_content()?;
36//!         self.prefix = config.prefix;
37//!         self.suffix = config.suffix;
38//!         self.installed_packages.push(package.info.clone());
39//!         Ok(())
40//!     }
41//!
42//!     fn uninstall_package(&mut self, package_id: &str) -> Result<(), PackageError> {
43//!         self.installed_packages.retain(|p| p.id != package_id);
44//!         self.prefix = String::new();
45//!         self.suffix = String::new();
46//!         Ok(())
47//!     }
48//! }
49//! ```
50
51use serde::{Deserialize, Serialize};
52use thiserror::Error;
53
54/// Errors that can occur during package operations.
55#[derive(Debug, Error)]
56pub enum PackageError {
57    /// Serialization/deserialization failed.
58    #[error("serialization error: {0}")]
59    Serialization(#[from] serde_json::Error),
60
61    /// Package not found.
62    #[error("package not found: {0}")]
63    NotFound(String),
64
65    /// Package already installed.
66    #[error("package already installed: {0}")]
67    AlreadyInstalled(String),
68
69    /// Invalid package data.
70    #[error("invalid package: {0}")]
71    Invalid(String),
72
73    /// Component does not support packages.
74    #[error("component does not support packages")]
75    NotSupported,
76
77    /// Install failed.
78    #[error("install failed: {0}")]
79    InstallFailed(String),
80
81    /// Uninstall failed.
82    #[error("uninstall failed: {0}")]
83    UninstallFailed(String),
84}
85
86/// Current package format version.
87pub const PACKAGE_VERSION: u32 = 1;
88
89/// Package metadata.
90///
91/// Contains identifying information about a package.
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93pub struct PackageInfo {
94    /// Unique package identifier.
95    pub id: String,
96
97    /// Human-readable name.
98    pub name: String,
99
100    /// Version string (semver recommended).
101    pub version: String,
102
103    /// Brief description.
104    pub description: String,
105
106    /// Whether the package is currently enabled.
107    pub enabled: bool,
108}
109
110impl PackageInfo {
111    /// Creates new package info.
112    #[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    /// Creates package info with enabled state.
129    #[must_use]
130    pub fn with_enabled(mut self, enabled: bool) -> Self {
131        self.enabled = enabled;
132        self
133    }
134}
135
136/// A package that can be installed into a component.
137///
138/// Contains metadata and content that modifies component behavior.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Package {
141    /// Package metadata.
142    pub info: PackageInfo,
143
144    /// Package format version.
145    pub version: u32,
146
147    /// Package content (component-specific configuration).
148    pub content: serde_json::Value,
149}
150
151impl Package {
152    /// Creates a new package from serializable content.
153    ///
154    /// # Arguments
155    ///
156    /// * `info` - Package metadata
157    /// * `content` - The content to serialize
158    ///
159    /// # Errors
160    ///
161    /// Returns `PackageError::Serialization` if the content cannot be serialized.
162    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    /// Creates a package with raw JSON content.
171    #[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    /// Deserializes the content.
181    ///
182    /// # Errors
183    ///
184    /// Returns `PackageError::Serialization` if deserialization fails.
185    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    /// Returns the package ID.
190    #[must_use]
191    pub fn id(&self) -> &str {
192        &self.info.id
193    }
194}
195
196/// Declares whether a component supports package installation.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
198pub enum PackageSupport {
199    /// Component supports package installation.
200    Enabled,
201
202    /// Component does not support packages (default).
203    #[default]
204    Disabled,
205}
206
207/// Trait for components that support package installation.
208///
209/// Implementing this trait allows a component to have packages
210/// installed that modify its behavior.
211///
212/// # Contract
213///
214/// - `list_packages()` returns currently installed packages
215/// - `install_package()` adds new behavior from a package
216/// - `uninstall_package()` removes installed package behavior
217pub trait Packageable {
218    /// Returns the list of installed packages.
219    fn list_packages(&self) -> &[PackageInfo];
220
221    /// Installs a package.
222    ///
223    /// # Errors
224    ///
225    /// Returns `PackageError` if installation fails.
226    fn install_package(&mut self, package: &Package) -> Result<(), PackageError>;
227
228    /// Uninstalls a package by ID.
229    ///
230    /// # Errors
231    ///
232    /// Returns `PackageError` if uninstallation fails.
233    fn uninstall_package(&mut self, package_id: &str) -> Result<(), PackageError>;
234
235    /// Returns whether a package is installed.
236    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        // Install
339        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        // Uninstall
346        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}