canic_template_runtime/storage/template/
manifest.rs1use 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#[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
52pub struct TemplateManifestStoreRecord {
53 pub entries: Vec<(TemplateReleaseKey, TemplateManifestRecord)>,
54}
55
56pub struct TemplateManifestStateStore;
61
62impl TemplateManifestStateStore {
63 pub fn upsert(release: TemplateReleaseKey, record: TemplateManifestRecord) {
65 TEMPLATE_MANIFESTS.with_borrow_mut(|map| {
66 map.insert(release, record);
67 });
68 }
69
70 #[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 pub fn clear() {
83 TEMPLATE_MANIFESTS.with_borrow_mut(BTreeMap::clear);
84 }
85
86 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}