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" => decode_migration_hex(value)
67 .map(Blob::from)
68 .map(ScalarLiteral::Blob),
69 "date" => Date::parse(value)
70 .map(ScalarLiteral::Date)
71 .ok_or_else(invalid),
72 "decimal" => Decimal::from_str(value)
73 .map(ScalarLiteral::Decimal)
74 .map_err(|_| invalid()),
75 "duration" => Duration::parse_flexible(value)
76 .map(ScalarLiteral::Duration)
77 .map_err(|_| invalid()),
78 "principal" => Principal::from_str(value)
79 .map(ScalarLiteral::Principal)
80 .map_err(|_| invalid()),
81 "subaccount" => {
82 let bytes = decode_migration_hex(value)?;
83 let bytes: [u8; 32] = bytes.try_into().map_err(|_| invalid())?;
84 Ok(ScalarLiteral::Subaccount(Subaccount::from_array(bytes)))
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
99fn decode_migration_hex(value: &str) -> Result<Vec<u8>, icydb_schema::SchemaContractError> {
100 if !value.len().is_multiple_of(2) {
101 return Err(icydb_schema::SchemaContractError::InvalidLiteral);
102 }
103 value
104 .as_bytes()
105 .as_chunks::<2>()
106 .0
107 .iter()
108 .map(|pair| {
109 let high = decode_hex_nibble(pair[0])?;
110 let low = decode_hex_nibble(pair[1])?;
111 Ok((high << 4) | low)
112 })
113 .collect()
114}
115
116const fn decode_hex_nibble(value: u8) -> Result<u8, icydb_schema::SchemaContractError> {
117 match value {
118 b'0'..=b'9' => Ok(value - b'0'),
119 b'a'..=b'f' => Ok(value - b'a' + 10),
120 b'A'..=b'F' => Ok(value - b'A' + 10),
121 _ => Err(icydb_schema::SchemaContractError::InvalidLiteral),
122 }
123}
124
125#[derive(Clone, Debug, Serialize)]
130pub struct Canister {
131 def: Def,
132 memory_namespace: &'static str,
133 memory_profile: CanisterMemoryProfile,
134 #[serde(skip)]
135 migration_plan: Option<MigrationPlanConstructor>,
136}
137
138impl Canister {
139 #[must_use]
140 pub const fn new(
141 def: Def,
142 memory_namespace: &'static str,
143 migration_plan: Option<MigrationPlanConstructor>,
144 ) -> Self {
145 Self {
146 def,
147 memory_namespace,
148 memory_profile: CanisterMemoryProfile::General,
149 migration_plan,
150 }
151 }
152
153 #[must_use]
154 pub const fn def(&self) -> &Def {
155 &self.def
156 }
157
158 #[must_use]
159 pub const fn memory_namespace(&self) -> &'static str {
160 self.memory_namespace
161 }
162
163 #[must_use]
167 pub const fn with_memory_profile(mut self, profile: CanisterMemoryProfile) -> Self {
168 self.memory_profile = profile;
169 self
170 }
171
172 #[must_use]
174 pub const fn memory_profile(&self) -> CanisterMemoryProfile {
175 self.memory_profile
176 }
177
178 pub fn migration_plan(
184 &self,
185 ) -> Result<Option<icydb_schema::SchemaMigrationPlan>, icydb_schema::SchemaContractError> {
186 self.migration_plan
187 .map(|constructor| constructor())
188 .transpose()
189 }
190
191 #[must_use]
192 pub fn commit_stable_key(&self) -> String {
193 stable_memory_key(self.memory_namespace(), "commit", "control")
194 }
195
196 #[must_use]
197 pub fn integrity_progress_stable_key(&self) -> String {
198 stable_memory_key(self.memory_namespace(), "integrity", "progress")
199 }
200
201 #[must_use]
202 pub fn startup_stable_key(&self) -> String {
203 stable_memory_key(self.memory_namespace(), "startup", "control")
204 }
205}
206
207impl MacroNode for Canister {
208 fn as_any(&self) -> &dyn std::any::Any {
209 self
210 }
211}
212
213impl ValidateNode for Canister {
214 fn validate(&self) -> Result<(), ErrorTree> {
215 let mut errs = ErrorTree::new();
216 validate_stable_key_segment(
217 &mut errs,
218 "canister memory_namespace",
219 self.memory_namespace(),
220 );
221 for key in [
222 self.commit_stable_key(),
223 self.startup_stable_key(),
224 self.integrity_progress_stable_key(),
225 ] {
226 validate_stable_key(&mut errs, "canister control key", &key);
227 }
228
229 let canister_path = self.def().path();
230 {
231 let schema = schema_read();
232 let mut seen = BTreeMap::new();
233 for (path, store) in
234 schema.filter_nodes::<Store>(|store| store.canister() == canister_path)
235 {
236 if let Some(config) = store.journaled_memory_config()
237 && let Some(previous) = seen.insert(config.key(), path)
238 {
239 err!(
240 errs,
241 "duplicate store key `{}` in canister `{}`: {} conflicts with {}",
242 config.key(),
243 canister_path,
244 previous,
245 path
246 );
247 }
248 }
249 }
250 errs.result()
251 }
252}
253
254impl VisitableNode for Canister {
255 fn route_key(&self) -> String {
256 self.def().path()
257 }
258}