1use std::collections::BTreeMap;
11
12use serde::Serialize;
13
14use crate::load::PluginMetadata;
15
16#[derive(Serialize, Debug)]
19pub struct Manifest {
20 pub plugin: PluginSection,
21 pub confium: ConfiumSection,
22 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
23 pub dependencies: BTreeMap<String, String>,
24 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
25 pub interfaces: BTreeMap<String, u8>,
26 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
27 pub algorithms: BTreeMap<String, Vec<String>>,
28 pub artifact: ArtifactSection,
29}
30
31#[derive(Serialize, Debug)]
32pub struct PluginSection {
33 pub name: String,
34 pub version: String,
35 pub publisher: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub license: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub homepage: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub source: Option<String>,
42}
43
44#[derive(Serialize, Debug)]
45pub struct ConfiumSection {
46 #[serde(rename = "contract-version")]
48 pub contract_version: u32,
49 #[serde(rename = "min-runtime")]
51 pub min_runtime: String,
52}
53
54#[derive(Serialize, Debug)]
55pub struct ArtifactSection {
56 pub url: String,
57 pub size: u64,
58 pub sha256: String,
59 #[serde(skip_serializing_if = "Vec::is_empty")]
60 pub mirrors: Vec<String>,
61}
62
63pub struct ManifestInput<'a> {
66 pub metadata: &'a PluginMetadata,
67 pub publisher: &'a str,
68 pub cli_name: Option<&'a str>,
69 pub cli_version: Option<&'a str>,
70 pub interfaces: &'a BTreeMap<String, u8>,
71 pub algorithms: &'a BTreeMap<String, Vec<String>>,
72 pub artifact_url: String,
73 pub artifact_size: u64,
74 pub artifact_sha256: String,
75 pub contract_version: u32,
76 pub min_runtime: &'a str,
77 pub mirrors: Vec<String>,
78}
79
80pub fn build(input: &ManifestInput<'_>) -> Manifest {
83 let name = input
84 .cli_name
85 .map(str::to_string)
86 .or_else(|| input.metadata.name.clone())
87 .expect("name resolved before build_manifest");
88 let version = input
89 .cli_version
90 .map(str::to_string)
91 .or_else(|| input.metadata.version.clone())
92 .expect("version resolved before build_manifest");
93
94 Manifest {
95 plugin: PluginSection {
96 name,
97 version,
98 publisher: input.publisher.to_string(),
99 license: input.metadata.license.clone(),
100 homepage: input.metadata.homepage.clone(),
101 source: input.metadata.source_url.clone(),
102 },
103 confium: ConfiumSection {
104 contract_version: input.contract_version,
105 min_runtime: input.min_runtime.to_string(),
106 },
107 dependencies: BTreeMap::new(),
108 interfaces: input.interfaces.clone(),
109 algorithms: input.algorithms.clone(),
110 artifact: ArtifactSection {
111 url: input.artifact_url.clone(),
112 size: input.artifact_size,
113 sha256: input.artifact_sha256.clone(),
114 mirrors: input.mirrors.clone(),
115 },
116 }
117}
118
119pub fn to_toml(manifest: &Manifest) -> Result<String, toml::ser::Error> {
121 toml::to_string_pretty(manifest)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn sample_metadata() -> PluginMetadata {
129 PluginMetadata {
130 name: Some("botan".into()),
131 version: Some("3.2.0".into()),
132 vendor: Some("Ribose".into()),
133 license: Some("BSD-2-Clause".into()),
134 homepage: Some("https://botan.randombit.net".into()),
135 description: Some("Botan crypto provider".into()),
136 source_url: Some("https://github.com/confium/confium-botan/tree/v3.2.0".into()),
137 }
138 }
139
140 #[test]
141 fn toml_contains_all_sections() {
142 let meta = sample_metadata();
143 let interfaces = BTreeMap::from([("hash".to_string(), 0u8)]);
144 let algorithms = BTreeMap::from([("hash".to_string(), vec!["SHA-256".to_string()])]);
145 let input = ManifestInput {
146 metadata: &meta,
147 publisher: "ribose",
148 cli_name: None,
149 cli_version: None,
150 interfaces: &interfaces,
151 algorithms: &algorithms,
152 artifact_url: "https://example.com/x.dylib".into(),
153 artifact_size: 1024,
154 artifact_sha256: "abc123".into(),
155 contract_version: 0,
156 min_runtime: "0.3.0",
157 mirrors: vec![],
158 };
159 let manifest = build(&input);
160 let toml = to_toml(&manifest).unwrap();
161 assert!(toml.contains("[plugin]"));
162 assert!(toml.contains("[confium]"));
163 assert!(toml.contains("[interfaces]"));
164 assert!(toml.contains("[algorithms]"));
165 assert!(toml.contains("[artifact]"));
166 assert!(toml.contains("name = \"botan\""));
167 assert!(toml.contains("sha256 = \"abc123\""));
168 }
169
170 #[test]
171 fn cli_overrides_take_precedence() {
172 let meta = sample_metadata();
173 let interfaces = BTreeMap::new();
174 let algorithms = BTreeMap::new();
175 let input = ManifestInput {
176 metadata: &meta,
177 publisher: "ribose",
178 cli_name: Some("my-plugin"),
179 cli_version: Some("9.9.9"),
180 interfaces: &interfaces,
181 algorithms: &algorithms,
182 artifact_url: "https://example.com/x.dylib".into(),
183 artifact_size: 1024,
184 artifact_sha256: "abc123".into(),
185 contract_version: 0,
186 min_runtime: "0.3.0",
187 mirrors: vec![],
188 };
189 let manifest = build(&input);
190 assert_eq!(manifest.plugin.name, "my-plugin");
191 assert_eq!(manifest.plugin.version, "9.9.9");
192 }
193
194 #[test]
195 fn empty_sections_are_skipped() {
196 let meta = sample_metadata();
197 let empty_ifaces = BTreeMap::new();
198 let empty_algos = BTreeMap::new();
199 let input = ManifestInput {
200 metadata: &meta,
201 publisher: "ribose",
202 cli_name: None,
203 cli_version: None,
204 interfaces: &empty_ifaces,
205 algorithms: &empty_algos,
206 artifact_url: "https://example.com/x.dylib".into(),
207 artifact_size: 1024,
208 artifact_sha256: "abc123".into(),
209 contract_version: 0,
210 min_runtime: "0.3.0",
211 mirrors: vec![],
212 };
213 let manifest = build(&input);
214 let toml = to_toml(&manifest).unwrap();
215 assert!(!toml.contains("[interfaces]"));
216 assert!(!toml.contains("[algorithms]"));
217 }
218}