Skip to main content

canic_template_runtime/storage/template/
manifest.rs

1use canic_cdk::structures::{BTreeMap, DefaultMemoryImpl, memory::VirtualMemory};
2use canic_memory::{eager_static, ic_memory, impl_storable_bounded};
3use canic_template_types::ids::{
4    CanisterRole, TemplateChunkingMode, TemplateManifestState, TemplateReleaseKey, TemplateVersion,
5    WasmStoreBinding,
6};
7use serde::{Deserialize, Serialize};
8use std::cell::RefCell;
9
10const TEMPLATE_MANIFESTS_ID: u8 = 10;
11
12eager_static! {
13    static TEMPLATE_MANIFESTS: RefCell<
14        BTreeMap<TemplateReleaseKey, TemplateManifestRecord, VirtualMemory<DefaultMemoryImpl>>
15    > = RefCell::new(
16        BTreeMap::init(ic_memory!(TemplateManifestStateStore, TEMPLATE_MANIFESTS_ID)),
17    );
18}
19
20///
21/// TemplateManifestRecord
22///
23
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25pub struct TemplateManifestRecord {
26    pub role: CanisterRole,
27    pub version: TemplateVersion,
28    pub payload_hash: Vec<u8>,
29    pub payload_size_bytes: u64,
30    pub store_binding: WasmStoreBinding,
31    pub chunking_mode: TemplateChunkingMode,
32    pub manifest_state: TemplateManifestState,
33    pub approved_at: Option<u64>,
34    pub created_at: u64,
35}
36
37impl TemplateManifestRecord {
38    pub const STORABLE_MAX_SIZE: u32 = 512;
39}
40
41impl_storable_bounded!(
42    TemplateManifestRecord,
43    TemplateManifestRecord::STORABLE_MAX_SIZE,
44    false
45);
46
47///
48/// TemplateManifestStoreRecord
49///
50
51#[derive(Clone, Debug, Default, Eq, PartialEq)]
52pub struct TemplateManifestStoreRecord {
53    pub entries: Vec<(TemplateReleaseKey, TemplateManifestRecord)>,
54}
55
56///
57/// TemplateManifestStateStore
58///
59
60pub struct TemplateManifestStateStore;
61
62impl TemplateManifestStateStore {
63    // Insert or replace a stored template manifest record.
64    pub fn upsert(release: TemplateReleaseKey, record: TemplateManifestRecord) {
65        TEMPLATE_MANIFESTS.with_borrow_mut(|map| {
66            map.insert(release, record);
67        });
68    }
69
70    // Export the full manifest snapshot for ops-owned filtering and shaping.
71    #[must_use]
72    pub fn export() -> TemplateManifestStoreRecord {
73        TEMPLATE_MANIFESTS.with_borrow(|map| TemplateManifestStoreRecord {
74            entries: map
75                .iter()
76                .map(|entry| (entry.key().clone(), entry.value()))
77                .collect(),
78        })
79    }
80
81    // Clear the manifest store.
82    pub fn clear() {
83        TEMPLATE_MANIFESTS.with_borrow_mut(BTreeMap::clear);
84    }
85
86    // Clear the manifest store for isolated unit tests.
87    pub fn clear_for_test() {
88        Self::clear();
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use canic_template_types::ids::TemplateId;
96
97    fn manifest() -> TemplateManifestRecord {
98        TemplateManifestRecord {
99            role: CanisterRole::new("app"),
100            version: TemplateVersion::new("0.18.0"),
101            payload_hash: vec![7; 32],
102            payload_size_bytes: 1024,
103            store_binding: WasmStoreBinding::new("primary"),
104            chunking_mode: TemplateChunkingMode::Inline,
105            manifest_state: TemplateManifestState::Approved,
106            approved_at: Some(42),
107            created_at: 41,
108        }
109    }
110
111    fn release() -> TemplateReleaseKey {
112        TemplateReleaseKey::new(
113            TemplateId::new("embedded:app"),
114            TemplateVersion::new("0.18.0"),
115        )
116    }
117
118    #[test]
119    fn manifest_store_round_trip() {
120        TemplateManifestStateStore::clear_for_test();
121        let record = manifest();
122
123        TemplateManifestStateStore::upsert(release(), record.clone());
124
125        let exported = TemplateManifestStateStore::export();
126        assert_eq!(exported.entries, vec![(release(), record)]);
127    }
128
129    #[test]
130    fn manifest_store_export_is_sorted_by_key_order() {
131        TemplateManifestStateStore::clear_for_test();
132
133        TemplateManifestStateStore::upsert(
134            TemplateReleaseKey::new(
135                TemplateId::new("embedded:z"),
136                TemplateVersion::new("0.18.0"),
137            ),
138            manifest(),
139        );
140        TemplateManifestStateStore::upsert(release(), manifest());
141
142        let exported = TemplateManifestStateStore::export();
143        assert_eq!(exported.entries.len(), 2);
144        assert_eq!(
145            exported.entries[0].0.template_id,
146            TemplateId::new("embedded:app")
147        );
148    }
149}