Skip to main content

icydb_schema/node/
canister.rs

1use crate::node::{validate_memory_id_in_range, validate_memory_id_not_reserved};
2use crate::prelude::*;
3use std::collections::BTreeMap;
4
5//
6// Canister
7//
8
9#[derive(CandidType, Clone, Debug, Serialize)]
10pub struct Canister {
11    def: Def,
12    memory_min: u8,
13    memory_max: u8,
14    pub commit_memory_id: u8,
15}
16
17impl Canister {
18    #[must_use]
19    pub const fn new(def: Def, memory_min: u8, memory_max: u8, commit_memory_id: u8) -> Self {
20        Self {
21            def,
22            memory_min,
23            memory_max,
24            commit_memory_id,
25        }
26    }
27
28    #[must_use]
29    pub const fn def(&self) -> &Def {
30        &self.def
31    }
32
33    #[must_use]
34    pub const fn memory_min(&self) -> u8 {
35        self.memory_min
36    }
37
38    #[must_use]
39    pub const fn memory_max(&self) -> u8 {
40        self.memory_max
41    }
42
43    #[must_use]
44    pub const fn commit_memory_id(&self) -> u8 {
45        self.commit_memory_id
46    }
47}
48
49impl MacroNode for Canister {
50    fn as_any(&self) -> &dyn std::any::Any {
51        self
52    }
53}
54
55impl ValidateNode for Canister {
56    fn validate(&self) -> Result<(), ErrorTree> {
57        let mut errs = ErrorTree::new();
58        let schema = schema_read();
59
60        let canister_path = self.def().path();
61        let mut seen_ids = BTreeMap::<u8, String>::new();
62
63        validate_memory_id_in_range(
64            &mut errs,
65            "commit_memory_id",
66            self.commit_memory_id(),
67            self.memory_min(),
68            self.memory_max(),
69        );
70        validate_memory_id_not_reserved(&mut errs, "commit_memory_id", self.commit_memory_id());
71
72        assert_unique_memory_id(
73            self.commit_memory_id(),
74            format!("Canister `{}`.commit_memory_id", self.def().path()),
75            &canister_path,
76            &mut seen_ids,
77            &mut errs,
78        );
79
80        // Check all Store nodes for this canister
81        for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
82            assert_unique_memory_id(
83                store.data_memory_id(),
84                format!("Store `{path}`.data_memory_id"),
85                &canister_path,
86                &mut seen_ids,
87                &mut errs,
88            );
89
90            assert_unique_memory_id(
91                store.index_memory_id(),
92                format!("Store `{path}`.index_memory_id"),
93                &canister_path,
94                &mut seen_ids,
95                &mut errs,
96            );
97
98            assert_unique_memory_id(
99                store.schema_memory_id(),
100                format!("Store `{path}`.schema_memory_id"),
101                &canister_path,
102                &mut seen_ids,
103                &mut errs,
104            );
105        }
106
107        errs.result()
108    }
109}
110
111fn assert_unique_memory_id(
112    memory_id: u8,
113    slot: String,
114    canister_path: &str,
115    seen_ids: &mut BTreeMap<u8, String>,
116    errs: &mut ErrorTree,
117) {
118    if let Some(existing) = seen_ids.get(&memory_id) {
119        err!(
120            errs,
121            "duplicate memory_id `{}` used in canister `{}`: {} conflicts with {}",
122            memory_id,
123            canister_path,
124            existing,
125            slot
126        );
127    } else {
128        seen_ids.insert(memory_id, slot);
129    }
130}
131
132impl VisitableNode for Canister {
133    fn route_key(&self) -> String {
134        self.def().path()
135    }
136}
137
138//
139// TESTS
140//
141
142#[cfg(test)]
143mod tests {
144    use crate::build::schema_write;
145
146    use super::*;
147
148    fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
149        let canister = Canister::new(Def::new(path_module, ident), 0, 255, 254);
150        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
151
152        canister
153    }
154
155    fn insert_store(
156        path_module: &'static str,
157        ident: &'static str,
158        canister_path: &'static str,
159        data_memory_id: u8,
160        index_memory_id: u8,
161        schema_memory_id: u8,
162    ) {
163        schema_write().insert_node(SchemaNode::Store(Store::new(
164            Def::new(path_module, ident),
165            ident,
166            canister_path,
167            data_memory_id,
168            index_memory_id,
169            schema_memory_id,
170        )));
171    }
172
173    #[test]
174    fn validate_rejects_memory_id_collision_between_stores() {
175        let canister = insert_canister("schema_store_collision", "Canister");
176        let canister_path = "schema_store_collision::Canister";
177
178        insert_store(
179            "schema_store_collision",
180            "StoreA",
181            canister_path,
182            10,
183            11,
184            12,
185        );
186        insert_store(
187            "schema_store_collision",
188            "StoreB",
189            canister_path,
190            13,
191            10,
192            14,
193        ); // collision
194
195        let err = canister
196            .validate()
197            .expect_err("memory-id collision must fail");
198
199        let rendered = err.to_string();
200        assert!(
201            rendered.contains("duplicate memory_id `10`"),
202            "expected duplicate memory-id error, got: {rendered}"
203        );
204    }
205
206    #[test]
207    fn validate_accepts_unique_memory_ids() {
208        let canister = insert_canister("schema_store_unique", "Canister");
209        let canister_path = "schema_store_unique::Canister";
210
211        insert_store("schema_store_unique", "StoreA", canister_path, 30, 31, 32);
212        insert_store("schema_store_unique", "StoreB", canister_path, 33, 34, 35);
213
214        canister.validate().expect("unique memory IDs should pass");
215    }
216
217    #[test]
218    fn validate_rejects_reserved_commit_memory_id() {
219        let canister = Canister::new(Def::new("schema_reserved_commit", "Canister"), 0, 255, 255);
220        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
221
222        let err = canister
223            .validate()
224            .expect_err("reserved commit memory id must fail");
225
226        let rendered = err.to_string();
227        assert!(
228            rendered.contains("reserved for stable-structures internals"),
229            "expected reserved-id error, got: {rendered}"
230        );
231    }
232}