icydb_model/node/
canister.rs1#[cfg(test)]
8mod tests;
9
10use crate::node::{stable_memory_key, validate_stable_key, validate_stable_key_segment};
11use crate::prelude::*;
12use std::collections::BTreeMap;
13use std::str::FromStr;
14
15pub type MigrationPlanConstructor =
17 fn() -> Result<icydb_schema::SchemaMigrationPlan, icydb_schema::SchemaContractError>;
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
28#[serde(rename_all = "snake_case")]
29pub enum CanisterMemoryProfile {
30 Compact,
32 General,
34 HighHeadroom,
36}
37
38impl CanisterMemoryProfile {
39 #[must_use]
41 pub const fn bucket_size_pages(self) -> u16 {
42 match self {
43 Self::Compact => 4,
44 Self::General => 16,
45 Self::HighHeadroom => 128,
46 }
47 }
48}
49
50#[doc(hidden)]
52pub fn migration_literal_from_text(
53 kind: &str,
54 value: &str,
55) -> Result<icydb_schema::ScalarLiteral, icydb_schema::SchemaContractError> {
56 use icydb_schema::{
57 Account, Blob, Date, Decimal, Duration, Principal, ScalarLiteral, Subaccount, Timestamp,
58 U256, Ulid,
59 };
60
61 let invalid = || icydb_schema::SchemaContractError::InvalidLiteral;
62 match kind {
63 "account" => Account::from_str(value)
64 .map(ScalarLiteral::Account)
65 .map_err(|_| invalid()),
66 "blob" => Blob::from_hex_literal(value).map(ScalarLiteral::Blob),
67 "date" => Date::parse(value)
68 .map(ScalarLiteral::Date)
69 .ok_or_else(invalid),
70 "decimal" => Decimal::from_str(value)
71 .map(ScalarLiteral::Decimal)
72 .map_err(|_| invalid()),
73 "duration" => Duration::parse_flexible(value)
74 .map(ScalarLiteral::Duration)
75 .map_err(|_| invalid()),
76 "principal" => Principal::from_str(value)
77 .map(ScalarLiteral::Principal)
78 .map_err(|_| invalid()),
79 "subaccount" => {
80 if value.len() != 64 {
82 return Err(invalid());
83 }
84 Subaccount::from_hex(value).map(ScalarLiteral::Subaccount)
85 }
86 "timestamp" => Timestamp::parse_flexible(value)
87 .map(ScalarLiteral::Timestamp)
88 .map_err(|_| invalid()),
89 "u256" => U256::from_str(value)
90 .map(ScalarLiteral::U256)
91 .map_err(|_| invalid()),
92 "ulid" => Ulid::from_str(value)
93 .map(ScalarLiteral::Ulid)
94 .map_err(|_| invalid()),
95 _ => Err(invalid()),
96 }
97}
98
99#[derive(Clone, Debug, Serialize)]
104pub struct Canister {
105 def: Def,
106 memory_namespace: &'static str,
107 memory_profile: CanisterMemoryProfile,
108 #[serde(skip)]
109 migration_plan: Option<MigrationPlanConstructor>,
110}
111
112impl Canister {
113 #[must_use]
114 pub const fn new(
115 def: Def,
116 memory_namespace: &'static str,
117 migration_plan: Option<MigrationPlanConstructor>,
118 ) -> Self {
119 Self {
120 def,
121 memory_namespace,
122 memory_profile: CanisterMemoryProfile::General,
123 migration_plan,
124 }
125 }
126
127 #[must_use]
128 pub const fn def(&self) -> &Def {
129 &self.def
130 }
131
132 #[must_use]
133 pub const fn memory_namespace(&self) -> &'static str {
134 self.memory_namespace
135 }
136
137 #[must_use]
141 pub const fn with_memory_profile(mut self, profile: CanisterMemoryProfile) -> Self {
142 self.memory_profile = profile;
143 self
144 }
145
146 #[must_use]
148 pub const fn memory_profile(&self) -> CanisterMemoryProfile {
149 self.memory_profile
150 }
151
152 pub fn migration_plan(
158 &self,
159 ) -> Result<Option<icydb_schema::SchemaMigrationPlan>, icydb_schema::SchemaContractError> {
160 self.migration_plan
161 .map(|constructor| constructor())
162 .transpose()
163 }
164
165 #[must_use]
166 pub fn commit_stable_key(&self) -> String {
167 stable_memory_key(self.memory_namespace(), "commit", "control")
168 }
169
170 #[must_use]
171 pub fn integrity_progress_stable_key(&self) -> String {
172 stable_memory_key(self.memory_namespace(), "integrity", "progress")
173 }
174
175 #[must_use]
176 pub fn startup_stable_key(&self) -> String {
177 stable_memory_key(self.memory_namespace(), "startup", "control")
178 }
179}
180
181impl MacroNode for Canister {
182 fn as_any(&self) -> &dyn std::any::Any {
183 self
184 }
185}
186
187impl ValidateNode for Canister {
188 fn validate(&self) -> Result<(), ErrorTree> {
189 let mut errs = ErrorTree::new();
190 validate_stable_key_segment(
191 &mut errs,
192 "canister memory_namespace",
193 self.memory_namespace(),
194 );
195 for key in [
196 self.commit_stable_key(),
197 self.startup_stable_key(),
198 self.integrity_progress_stable_key(),
199 ] {
200 validate_stable_key(&mut errs, "canister control key", &key);
201 }
202
203 let canister_path = self.def().path();
204 {
205 let schema = schema_read();
206 let mut seen = BTreeMap::new();
207 for (path, store) in
208 schema.filter_nodes::<Store>(|store| store.canister() == canister_path)
209 {
210 if let Some(config) = store.journaled_memory_config()
211 && let Some(previous) = seen.insert(config.key(), path)
212 {
213 err!(
214 errs,
215 "duplicate store key `{}` in canister `{}`: {} conflicts with {}",
216 config.key(),
217 canister_path,
218 previous,
219 path
220 );
221 }
222 }
223 }
224 errs.result()
225 }
226}
227
228impl VisitableNode for Canister {
229 fn route_key(&self) -> String {
230 self.def().path()
231 }
232}