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