Skip to main content

confium_publish/
manifest.rs

1// Generate the registry `manifest.toml` for a published plugin version.
2//
3// and mirrored by the example at
4// `sites/registry/plugins/botan/3.2.0/manifest.toml`. This module owns
5// the serde model that produces that shape and nothing else — signing,
6// hashing, and directory layout live in their sibling modules
7// (Open/Closed: extend the model here, change the wire shape via the
8// struct, never hand-roll serialization).
9
10use std::collections::BTreeMap;
11
12use serde::Serialize;
13
14use crate::load::PluginMetadata;
15
16/// Top-level manifest model. Field order follows the example manifest so
17/// `toml::to_string_pretty` emits sections in the conventional order.
18#[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    /// The `cfmp_interface_version` the plugin speaks (currently 0).
47    #[serde(rename = "contract-version")]
48    pub contract_version: u32,
49    /// Minimum Confium runtime version required to load this plugin.
50    #[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
63/// Inputs gathered from FFI + CLI before serialization. Building this
64/// explicitly keeps `build` a pure function of its arguments.
65pub 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
80/// Build a `Manifest` from resolved inputs. FFI metadata supplies
81/// defaults; CLI flags override `name`/`version` when present.
82pub 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
119/// Serialize a `Manifest` to the pretty TOML string written to disk.
120pub 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}