roboticus_plugin_sdk/
catalog.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct PluginCatalogEntry {
10 pub name: String,
12 pub version: String,
14 pub description: String,
16 #[serde(default)]
18 pub author: String,
19 pub sha256: String,
21 pub path: String,
23 #[serde(default)]
25 pub min_version: Option<String>,
26 #[serde(default = "default_tier")]
28 pub tier: String,
29}
30
31fn default_tier() -> String {
32 "community".to_string()
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct PluginCatalog {
38 #[serde(default)]
40 pub catalog: Vec<PluginCatalogEntry>,
41}
42
43impl PluginCatalog {
44 pub fn search(&self, query: &str) -> Vec<&PluginCatalogEntry> {
46 let q = query.to_lowercase();
47 self.catalog
48 .iter()
49 .filter(|e| {
50 e.name.to_lowercase().contains(&q)
51 || e.description.to_lowercase().contains(&q)
52 || e.author.to_lowercase().contains(&q)
53 })
54 .collect()
55 }
56
57 pub fn find(&self, name: &str) -> Option<&PluginCatalogEntry> {
59 self.catalog.iter().find(|e| e.name == name)
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 fn sample_catalog() -> PluginCatalog {
68 PluginCatalog {
69 catalog: vec![
70 PluginCatalogEntry {
71 name: "claude-code".into(),
72 version: "0.1.0".into(),
73 description: "Delegate coding tasks to Claude Code CLI".into(),
74 author: "Roboticus".into(),
75 sha256: "abc123".into(),
76 path: "plugins/claude-code-0.1.0.ic.zip".into(),
77 min_version: Some("0.9.4".into()),
78 tier: "official".into(),
79 },
80 PluginCatalogEntry {
81 name: "weather".into(),
82 version: "1.0.0".into(),
83 description: "Check weather forecasts".into(),
84 author: "Community".into(),
85 sha256: "def456".into(),
86 path: "plugins/weather-1.0.0.ic.zip".into(),
87 min_version: None,
88 tier: "community".into(),
89 },
90 ],
91 }
92 }
93
94 #[test]
95 fn search_by_name() {
96 let cat = sample_catalog();
97 let results = cat.search("claude");
98 assert_eq!(results.len(), 1);
99 assert_eq!(results[0].name, "claude-code");
100 }
101
102 #[test]
103 fn search_by_description() {
104 let cat = sample_catalog();
105 let results = cat.search("forecast");
106 assert_eq!(results.len(), 1);
107 assert_eq!(results[0].name, "weather");
108 }
109
110 #[test]
111 fn search_case_insensitive() {
112 let cat = sample_catalog();
113 let results = cat.search("CLAUDE");
114 assert_eq!(results.len(), 1);
115 }
116
117 #[test]
118 fn find_exact() {
119 let cat = sample_catalog();
120 assert!(cat.find("weather").is_some());
121 assert!(cat.find("nonexistent").is_none());
122 }
123
124 #[test]
125 fn empty_search_returns_all() {
126 let cat = sample_catalog();
127 let results = cat.search("");
128 assert_eq!(results.len(), 2);
129 }
130
131 #[test]
132 fn serde_roundtrip() {
133 let cat = sample_catalog();
134 let json = serde_json::to_string(&cat).unwrap();
135 let back: PluginCatalog = serde_json::from_str(&json).unwrap();
136 assert_eq!(back.catalog.len(), 2);
137 assert_eq!(back.catalog[0].tier, "official");
138 }
139}