Skip to main content

confium_coordinator/
marketplace.rs

1//! Plugin marketplace schema — discovery and registry format.
2
3use serde::{Deserialize, Serialize};
4
5/// Marketplace entry for a plugin.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct MarketplaceEntry {
8    pub manifest: crate::plugin_manifest::PluginManifest,
9    pub registry: RegistryInfo,
10    pub install: InstallInfo,
11    pub verification: VerificationInfo,
12}
13
14/// Registry-side metadata.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RegistryInfo {
17    pub registry_name: String,
18    pub published_at: chrono::DateTime<chrono::Utc>,
19    pub download_count: u64,
20    pub featured: bool,
21}
22
23/// Installation instructions.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct InstallInfo {
26    pub command: String,
27    pub binary_url: Option<String>,
28    pub checksum_sha256: Option<String>,
29}
30
31/// Signature verification info.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct VerificationInfo {
34    pub signed_by: String,
35    pub signature_algorithm: String,
36    pub signature_hex: String,
37    pub verified: bool,
38}
39
40/// Marketplace search query.
41#[derive(Debug, Clone, Serialize, Deserialize, Default)]
42pub struct SearchQuery {
43    pub query: Option<String>,
44    pub interface: Option<String>,
45    pub algorithm: Option<String>,
46    pub min_version: Option<String>,
47}
48
49/// Match an entry against a search query.
50pub fn matches(entry: &MarketplaceEntry, query: &SearchQuery) -> bool {
51    if let Some(ref q) = query.query {
52        let q_lower = q.to_lowercase();
53        if !entry.manifest.name.to_lowercase().contains(&q_lower)
54            && !entry.manifest.description.to_lowercase().contains(&q_lower)
55        {
56            return false;
57        }
58    }
59    if let Some(ref iface) = query.interface {
60        if !entry.manifest.interfaces.iter().any(|i| i == iface) {
61            return false;
62        }
63    }
64    if let Some(ref alg) = query.algorithm {
65        if !entry.manifest.algorithms.iter().any(|a| a == alg) {
66            return false;
67        }
68    }
69    true
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::plugin_manifest::PluginManifest;
76
77    fn make_entry(name: &str) -> MarketplaceEntry {
78        MarketplaceEntry {
79            manifest: PluginManifest {
80                name: name.into(),
81                version: "1.0.0".into(),
82                description: "A test plugin".into(),
83                author: "Test".into(),
84                license: "MIT".into(),
85                interfaces: vec!["hash".into()],
86                dependencies: vec![],
87                algorithms: vec!["SHA-256".into()],
88                homepage: None,
89            },
90            registry: RegistryInfo {
91                registry_name: "official".into(),
92                published_at: chrono::Utc::now(),
93                download_count: 100,
94                featured: false,
95            },
96            install: InstallInfo {
97                command: "confium install test".into(),
98                binary_url: None,
99                checksum_sha256: None,
100            },
101            verification: VerificationInfo {
102                signed_by: "confium-bot".into(),
103                signature_algorithm: "Ed25519".into(),
104                signature_hex: "abc123".into(),
105                verified: true,
106            },
107        }
108    }
109
110    #[test]
111    fn entry_serializes() {
112        let entry = make_entry("test");
113        let json = serde_json::to_string(&entry).unwrap();
114        assert!(json.contains("test"));
115        assert!(json.contains("registry"));
116    }
117
118    #[test]
119    fn search_by_name() {
120        let entry = make_entry("my-hash-plugin");
121        let query = SearchQuery {
122            query: Some("hash".into()),
123            ..Default::default()
124        };
125        assert!(matches(&entry, &query));
126    }
127
128    #[test]
129    fn search_no_match() {
130        let entry = make_entry("crypto-plugin");
131        let query = SearchQuery {
132            query: Some("networking".into()),
133            ..Default::default()
134        };
135        assert!(!matches(&entry, &query));
136    }
137
138    #[test]
139    fn search_by_interface() {
140        let entry = make_entry("x");
141        let query = SearchQuery {
142            interface: Some("hash".into()),
143            ..Default::default()
144        };
145        assert!(matches(&entry, &query));
146    }
147
148    #[test]
149    fn search_by_algorithm() {
150        let entry = make_entry("x");
151        let query = SearchQuery {
152            algorithm: Some("SHA-256".into()),
153            ..Default::default()
154        };
155        assert!(matches(&entry, &query));
156    }
157
158    #[test]
159    fn search_wrong_interface() {
160        let entry = make_entry("x");
161        let query = SearchQuery {
162            interface: Some("aead".into()),
163            ..Default::default()
164        };
165        assert!(!matches(&entry, &query));
166    }
167
168    #[test]
169    fn empty_query_matches_all() {
170        let entry = make_entry("anything");
171        assert!(matches(&entry, &SearchQuery::default()));
172    }
173
174    #[test]
175    fn combined_filters() {
176        let entry = make_entry("hash-plugin");
177        let query = SearchQuery {
178            query: Some("hash".into()),
179            interface: Some("hash".into()),
180            ..Default::default()
181        };
182        assert!(matches(&entry, &query));
183    }
184}