use crate::node::{validate_memory_id_in_range, validate_memory_id_not_reserved};
use crate::prelude::*;
use std::collections::BTreeMap;
#[derive(CandidType, Clone, Debug, Serialize)]
pub struct Canister {
def: Def,
memory_min: u8,
memory_max: u8,
pub commit_memory_id: u8,
}
impl Canister {
#[must_use]
pub const fn new(def: Def, memory_min: u8, memory_max: u8, commit_memory_id: u8) -> Self {
Self {
def,
memory_min,
memory_max,
commit_memory_id,
}
}
#[must_use]
pub const fn def(&self) -> &Def {
&self.def
}
#[must_use]
pub const fn memory_min(&self) -> u8 {
self.memory_min
}
#[must_use]
pub const fn memory_max(&self) -> u8 {
self.memory_max
}
#[must_use]
pub const fn commit_memory_id(&self) -> u8 {
self.commit_memory_id
}
}
impl MacroNode for Canister {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ValidateNode for Canister {
fn validate(&self) -> Result<(), ErrorTree> {
let mut errs = ErrorTree::new();
let schema = schema_read();
let canister_path = self.def().path();
let mut seen_ids = BTreeMap::<u8, String>::new();
validate_memory_id_in_range(
&mut errs,
"commit_memory_id",
self.commit_memory_id(),
self.memory_min(),
self.memory_max(),
);
validate_memory_id_not_reserved(&mut errs, "commit_memory_id", self.commit_memory_id());
assert_unique_memory_id(
self.commit_memory_id(),
format!("Canister `{}`.commit_memory_id", self.def().path()),
&canister_path,
&mut seen_ids,
&mut errs,
);
for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
assert_unique_memory_id(
store.data_memory_id(),
format!("Store `{path}`.data_memory_id"),
&canister_path,
&mut seen_ids,
&mut errs,
);
assert_unique_memory_id(
store.index_memory_id(),
format!("Store `{path}`.index_memory_id"),
&canister_path,
&mut seen_ids,
&mut errs,
);
}
errs.result()
}
}
fn assert_unique_memory_id(
memory_id: u8,
slot: String,
canister_path: &str,
seen_ids: &mut BTreeMap<u8, String>,
errs: &mut ErrorTree,
) {
if let Some(existing) = seen_ids.get(&memory_id) {
err!(
errs,
"duplicate memory_id `{}` used in canister `{}`: {} conflicts with {}",
memory_id,
canister_path,
existing,
slot
);
} else {
seen_ids.insert(memory_id, slot);
}
}
impl VisitableNode for Canister {
fn route_key(&self) -> String {
self.def().path()
}
}
#[cfg(test)]
mod tests {
use crate::build::schema_write;
use super::*;
fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
let canister = Canister::new(Def::new(path_module, ident), 0, 255, 254);
schema_write().insert_node(SchemaNode::Canister(canister.clone()));
canister
}
fn insert_store(
path_module: &'static str,
ident: &'static str,
canister_path: &'static str,
data_memory_id: u8,
index_memory_id: u8,
) {
schema_write().insert_node(SchemaNode::Store(Store::new(
Def::new(path_module, ident),
ident,
canister_path,
data_memory_id,
index_memory_id,
)));
}
#[test]
fn validate_rejects_memory_id_collision_between_stores() {
let canister = insert_canister("schema_store_collision", "Canister");
let canister_path = "schema_store_collision::Canister";
insert_store("schema_store_collision", "StoreA", canister_path, 10, 11);
insert_store("schema_store_collision", "StoreB", canister_path, 12, 10);
let err = canister
.validate()
.expect_err("memory-id collision must fail");
let rendered = err.to_string();
assert!(
rendered.contains("duplicate memory_id `10`"),
"expected duplicate memory-id error, got: {rendered}"
);
}
#[test]
fn validate_accepts_unique_memory_ids() {
let canister = insert_canister("schema_store_unique", "Canister");
let canister_path = "schema_store_unique::Canister";
insert_store("schema_store_unique", "StoreA", canister_path, 30, 31);
insert_store("schema_store_unique", "StoreB", canister_path, 32, 33);
canister.validate().expect("unique memory IDs should pass");
}
#[test]
fn validate_rejects_reserved_commit_memory_id() {
let canister = Canister::new(Def::new("schema_reserved_commit", "Canister"), 0, 255, 255);
schema_write().insert_node(SchemaNode::Canister(canister.clone()));
let err = canister
.validate()
.expect_err("reserved commit memory id must fail");
let rendered = err.to_string();
assert!(
rendered.contains("reserved for stable-structures internals"),
"expected reserved-id error, got: {rendered}"
);
}
}