1use serde::{Deserialize, Serialize};
6
7#[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#[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#[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#[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#[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 pub fn get_extension(&self, id: &str) -> Option<&ExtensionInfo> {
99 self.extensions.iter().find(|e| e.id == id)
100 }
101
102 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 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 pub fn get_category(&self, id: &str) -> Option<&Category> {
120 self.categories.iter().find(|c| c.id == id)
121 }
122
123 pub fn get_client_app(&self, id: &str) -> Option<&ClientApp> {
125 self.client_apps.iter().find(|a| a.id == id)
126 }
127}
128
129#[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#[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#[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}