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
99        errs.result()
100    }
101}
102
103fn assert_unique_memory_id(
104    memory_id: u8,
105    slot: String,
106    canister_path: &str,
107    seen_ids: &mut BTreeMap<u8, String>,
108    errs: &mut ErrorTree,
109) {
110    if let Some(existing) = seen_ids.get(&memory_id) {
111        err!(
112            errs,
113            "duplicate memory_id `{}` used in canister `{}`: {} conflicts with {}",
114            memory_id,
115            canister_path,
116            existing,
117            slot
118        );
119    } else {
120        seen_ids.insert(memory_id, slot);
121    }
122}
123
124impl VisitableNode for Canister {
125    fn route_key(&self) -> String {
126        self.def().path()
127    }
128}
129
130///
131/// TESTS
132///
133
134#[cfg(test)]
135mod tests {
136    use crate::build::schema_write;
137
138    use super::*;
139
140    fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
141        let canister = Canister::new(Def::new(path_module, ident, None), 0, 255, 254);
142        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
143
144        canister
145    }
146
147    fn insert_store(
148        path_module: &'static str,
149        ident: &'static str,
150        canister_path: &'static str,
151        data_memory_id: u8,
152        index_memory_id: u8,
153    ) {
154        schema_write().insert_node(SchemaNode::Store(Store::new(
155            Def::new(path_module, ident, None),
156            ident,
157            canister_path,
158            data_memory_id,
159            index_memory_id,
160        )));
161    }
162
163    #[test]
164    fn validate_rejects_memory_id_collision_between_stores() {
165        let canister = insert_canister("schema_store_collision", "Canister");
166        let canister_path = "schema_store_collision::Canister";
167
168        insert_store("schema_store_collision", "StoreA", canister_path, 10, 11);
169        insert_store("schema_store_collision", "StoreB", canister_path, 12, 10); // collision
170
171        let err = canister
172            .validate()
173            .expect_err("memory-id collision must fail");
174
175        let rendered = err.to_string();
176        assert!(
177            rendered.contains("duplicate memory_id `10`"),
178            "expected duplicate memory-id error, got: {rendered}"
179        );
180    }
181
182    #[test]
183    fn validate_accepts_unique_memory_ids() {
184        let canister = insert_canister("schema_store_unique", "Canister");
185        let canister_path = "schema_store_unique::Canister";
186
187        insert_store("schema_store_unique", "StoreA", canister_path, 30, 31);
188        insert_store("schema_store_unique", "StoreB", canister_path, 32, 33);
189
190        canister.validate().expect("unique memory IDs should pass");
191    }
192
193    #[test]
194    fn validate_rejects_reserved_commit_memory_id() {
195        let canister = Canister::new(
196            Def::new("schema_reserved_commit", "Canister", None),
197            0,
198            255,
199            255,
200        );
201        schema_write().insert_node(SchemaNode::Canister(canister.clone()));
202
203        let err = canister
204            .validate()
205            .expect_err("reserved commit memory id must fail");
206
207        let rendered = err.to_string();
208        assert!(
209            rendered.contains("reserved for stable-structures internals"),
210            "expected reserved-id error, got: {rendered}"
211        );
212    }
213}