Skip to main content

pw_core/
extensions.rs

1//! Extension Registry Types
2//!
3//! Types for the PromptWallet extension system.
4
5use serde::{Deserialize, Serialize};
6
7// ============================================================================
8// Extension Info
9// ============================================================================
10
11/// Extension metadata (from registry)
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExtensionInfo {
14    pub id: String,
15    pub name: String,
16    pub tagline: String,
17    pub description: String,
18    pub icon: String,
19    pub category: String,
20    pub status: ExtensionStatus,
21    pub features: Vec<String>,
22    #[serde(rename = "requiredBy")]
23    pub required_by: Vec<String>,
24    pub docs: String,
25    pub pricing: String,
26}
27
28/// Extension status
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum ExtensionStatus {
32    Planned,
33    Beta,
34    Stable,
35    Deprecated,
36}
37
38impl ExtensionStatus {
39    pub fn as_str(&self) -> &'static str {
40        match self {
41            ExtensionStatus::Planned => "planned",
42            ExtensionStatus::Beta => "beta",
43            ExtensionStatus::Stable => "stable",
44            ExtensionStatus::Deprecated => "deprecated",
45        }
46    }
47}
48
49impl std::fmt::Display for ExtensionStatus {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}", self.as_str())
52    }
53}
54
55// ============================================================================
56// Category
57// ============================================================================
58
59/// Extension category
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Category {
62    pub id: String,
63    pub name: String,
64    pub description: String,
65    pub icon: String,
66}
67
68// ============================================================================
69// Client App
70// ============================================================================
71
72/// Client application configuration
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ClientApp {
75    pub id: String,
76    pub name: String,
77    pub description: String,
78    pub requires: Vec<String>,
79    pub optional: Vec<String>,
80}
81
82// ============================================================================
83// Registry
84// ============================================================================
85
86/// Full extension registry
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ExtensionRegistry {
89    pub version: String,
90    pub extensions: Vec<ExtensionInfo>,
91    pub categories: Vec<Category>,
92    #[serde(rename = "clientApps")]
93    pub client_apps: Vec<ClientApp>,
94}
95
96impl ExtensionRegistry {
97    /// Get extension by ID
98    pub fn get_extension(&self, id: &str) -> Option<&ExtensionInfo> {
99        self.extensions.iter().find(|e| e.id == id)
100    }
101
102    /// Get extensions by category
103    pub fn get_by_category(&self, category: &str) -> Vec<&ExtensionInfo> {
104        self.extensions
105            .iter()
106            .filter(|e| e.category == category)
107            .collect()
108    }
109
110    /// Get extensions by status
111    pub fn get_by_status(&self, status: ExtensionStatus) -> Vec<&ExtensionInfo> {
112        self.extensions
113            .iter()
114            .filter(|e| e.status == status)
115            .collect()
116    }
117
118    /// Get category by ID
119    pub fn get_category(&self, id: &str) -> Option<&Category> {
120        self.categories.iter().find(|c| c.id == id)
121    }
122
123    /// Get client app by ID
124    pub fn get_client_app(&self, id: &str) -> Option<&ClientApp> {
125        self.client_apps.iter().find(|a| a.id == id)
126    }
127}
128
129// ============================================================================
130// API Response Types
131// ============================================================================
132
133/// Response for listing extensions
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ExtensionsListResponse {
136    pub extensions: Vec<ExtensionInfo>,
137    pub categories: Vec<Category>,
138    #[serde(rename = "clientApps")]
139    pub client_apps: Vec<ClientApp>,
140    pub version: String,
141}
142
143/// Response for extension details
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ExtensionDetailResponse {
146    pub extension: ExtensionInfo,
147    pub category: Option<Category>,
148    pub required_by_apps: Vec<String>,
149    pub is_loaded: bool,
150}
151
152// ============================================================================
153// Tests
154// ============================================================================
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_extension_status_serialization() {
162        let statuses = vec![
163            (ExtensionStatus::Planned, "\"planned\""),
164            (ExtensionStatus::Beta, "\"beta\""),
165            (ExtensionStatus::Stable, "\"stable\""),
166            (ExtensionStatus::Deprecated, "\"deprecated\""),
167        ];
168
169        for (status, expected) in statuses {
170            let json = serde_json::to_string(&status).unwrap();
171            assert_eq!(json, expected);
172        }
173    }
174
175    #[test]
176    fn test_extension_info_serialization() {
177        let info = ExtensionInfo {
178            id: "pw-workspace".to_string(),
179            name: "Workspace".to_string(),
180            tagline: "Index code".to_string(),
181            description: "Full description".to_string(),
182            icon: "folder".to_string(),
183            category: "coding".to_string(),
184            status: ExtensionStatus::Stable,
185            features: vec!["Feature 1".to_string()],
186            required_by: vec!["app1".to_string()],
187            docs: "/docs/workspace.md".to_string(),
188            pricing: "included".to_string(),
189        };
190
191        let json = serde_json::to_value(&info).unwrap();
192        assert_eq!(json["id"], "pw-workspace");
193        assert_eq!(json["status"], "stable");
194        assert_eq!(json["requiredBy"][0], "app1");
195    }
196
197    #[test]
198    fn test_registry_lookup() {
199        let registry = ExtensionRegistry {
200            version: "1.0".to_string(),
201            extensions: vec![
202                ExtensionInfo {
203                    id: "ext1".to_string(),
204                    name: "Extension 1".to_string(),
205                    tagline: "...".to_string(),
206                    description: "...".to_string(),
207                    icon: "icon".to_string(),
208                    category: "coding".to_string(),
209                    status: ExtensionStatus::Stable,
210                    features: vec![],
211                    required_by: vec![],
212                    docs: "".to_string(),
213                    pricing: "free".to_string(),
214                },
215            ],
216            categories: vec![
217                Category {
218                    id: "coding".to_string(),
219                    name: "Coding".to_string(),
220                    description: "Dev tools".to_string(),
221                    icon: "code".to_string(),
222                },
223            ],
224            client_apps: vec![],
225        };
226
227        assert!(registry.get_extension("ext1").is_some());
228        assert!(registry.get_extension("nonexistent").is_none());
229        assert_eq!(registry.get_by_category("coding").len(), 1);
230        assert!(registry.get_category("coding").is_some());
231    }
232}