Skip to main content

alef_component_runtime/
manifest.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::path::{Path, PathBuf};
4
5pub const COMPONENT_MANIFEST_SCHEMA: u32 = 2;
6pub const COMPONENT_ABI_VERSION: u32 = 1;
7
8/// One contract a component provides, alongside the Rust type implementing it.
9///
10/// A component may provide more than one contract (one producer entrypoint per
11/// contract); each entry keeps its own `contract_hash` so a change to any single
12/// contract's ABI is independently detectable.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct ComponentProvidedContract {
16    pub contract: String,
17    pub interface_version: u32,
18    pub contract_hash: String,
19    pub implementation: String,
20}
21
22/// Whether a component is downloaded on demand or must be linked into the binding
23/// ahead of time. `alef`'s config-resolution code (`alef::codegen::component`) re-exports
24/// this type and exposes it on the resolved component config as a forward-looking
25/// hook; no loader or backend acts on `Bundled` yet.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum ComponentDeliveryMode {
29    Download,
30    Bundled,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct ComponentIdentity {
36    pub crate_name: String,
37    pub component: String,
38    pub version: String,
39    pub target: String,
40    pub feature_hash: String,
41    /// Hash of the first (lexicographically, by contract name) contract this
42    /// component provides. For a component providing exactly one contract --
43    /// the only shape the v1 loader fully validates today -- this is that
44    /// contract's real hash. See [`ComponentManifest::provides`] for the
45    /// complete, per-contract list.
46    pub contract_hash: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct ComponentLibrary {
52    pub file: String,
53    pub sha256: String,
54    pub size: u64,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct ComponentManifest {
60    pub schema_version: u32,
61    pub abi_version: u32,
62    pub identity: ComponentIdentity,
63    /// Every contract this component provides, sorted by contract name.
64    pub provides: Vec<ComponentProvidedContract>,
65    pub features: Vec<String>,
66    pub default_features: bool,
67    pub library: ComponentLibrary,
68}
69
70impl ComponentManifest {
71    pub fn canonical_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
72        canonical_json(self)
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct ComponentSignature {
79    pub algorithm: String,
80    pub key_id: String,
81    pub signature: String,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct ComponentArtifactRecord {
87    pub manifest: ComponentManifest,
88    pub manifest_sha256: String,
89    pub archive: String,
90    pub archive_sha256: String,
91    pub archive_size: u64,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub signature: Option<ComponentSignature>,
94}
95
96impl ComponentArtifactRecord {
97    #[must_use]
98    pub fn record_path(&self, output_dir: &Path) -> PathBuf {
99        output_dir.join(format!("{}.record.json", self.archive))
100    }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(deny_unknown_fields)]
105pub struct ComponentLock {
106    pub schema_version: u32,
107    pub public_keys: BTreeMap<String, String>,
108    pub artifacts: Vec<ComponentLockEntry>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct ComponentLockEntry {
114    pub identity: ComponentIdentity,
115    /// Every contract this component provides, mirrored from the manifest so
116    /// a consumer can inspect them without downloading and parsing the archive.
117    pub provides: Vec<ComponentProvidedContract>,
118    /// Whether `identity.target` downloads this artifact or must link it into the
119    /// binding. Every entry produced by `alef component lock` today is `Download`;
120    /// no backend builds or locks a `Bundled` artifact yet.
121    pub mode: ComponentDeliveryMode,
122    pub url: String,
123    pub sha256: String,
124    pub size: u64,
125    pub manifest_sha256: String,
126    pub key_id: String,
127}
128
129pub fn canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
130    let mut bytes = serde_json::to_vec(value)?;
131    bytes.push(b'\n');
132    Ok(bytes)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn canonical_json_is_compact_and_newline_terminated() {
141        let identity = ComponentIdentity {
142            crate_name: "demo-core".into(),
143            component: "fast".into(),
144            version: "1.0.0".into(),
145            target: "aarch64-apple-darwin".into(),
146            feature_hash: "11".repeat(32),
147            contract_hash: "22".repeat(32),
148        };
149        let bytes = canonical_json(&identity).unwrap();
150        assert!(bytes.ends_with(b"\n"));
151        assert!(!bytes.contains(&b' '));
152    }
153
154    #[test]
155    fn lock_rejects_unknown_fields() {
156        let json = r#"{"schema_version":1,"public_keys":{},"artifacts":[],"extra":true}"#;
157        assert!(serde_json::from_str::<ComponentLock>(json).is_err());
158    }
159}