1#[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, IntBig, NatBig, Principal, ScalarLiteral,
58 Subaccount, Timestamp, 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 "int_big" => IntBig::from_str(value)
79 .map(ScalarLiteral::IntBig)
80 .map_err(|_| invalid()),
81 "nat_big" => NatBig::from_str(value)
82 .map(ScalarLiteral::NatBig)
83 .map_err(|_| invalid()),
84 "principal" => Principal::from_str(value)
85 .map(ScalarLiteral::Principal)
86 .map_err(|_| invalid()),
87 "subaccount" => {
88 let bytes = decode_migration_hex(value)?;
89 let bytes: [u8; 32] = bytes.try_into().map_err(|_| invalid())?;
90 Ok(ScalarLiteral::Subaccount(Subaccount::from_array(bytes)))
91 }
92 "timestamp" => Timestamp::parse_flexible(value)
93 .map(ScalarLiteral::Timestamp)
94 .map_err(|_| invalid()),
95 "u256" => U256::from_str(value)
96 .map(ScalarLiteral::U256)
97 .map_err(|_| invalid()),
98 "ulid" => Ulid::from_str(value)
99 .map(ScalarLiteral::Ulid)
100 .map_err(|_| invalid()),
101 _ => Err(invalid()),
102 }
103}
104
105fn decode_migration_hex(value: &str) -> Result<Vec<u8>, icydb_schema::SchemaContractError> {
106 if !value.len().is_multiple_of(2) {
107 return Err(icydb_schema::SchemaContractError::InvalidLiteral);
108 }
109 value
110 .as_bytes()
111 .as_chunks::<2>()
112 .0
113 .iter()
114 .map(|pair| {
115 let high = decode_hex_nibble(pair[0])?;
116 let low = decode_hex_nibble(pair[1])?;
117 Ok((high << 4) | low)
118 })
119 .collect()
120}
121
122const fn decode_hex_nibble(value: u8) -> Result<u8, icydb_schema::SchemaContractError> {
123 match value {
124 b'0'..=b'9' => Ok(value - b'0'),
125 b'a'..=b'f' => Ok(value - b'a' + 10),
126 b'A'..=b'F' => Ok(value - b'A' + 10),
127 _ => Err(icydb_schema::SchemaContractError::InvalidLiteral),
128 }
129}
130
131#[derive(Clone, Debug, Serialize)]
136pub struct Canister {
137 def: Def,
138 memory_namespace: &'static str,
139 memory_profile: CanisterMemoryProfile,
140 #[serde(skip)]
141 migration_plan: Option<MigrationPlanConstructor>,
142}
143
144impl Canister {
145 #[must_use]
146 pub const fn new(
147 def: Def,
148 memory_namespace: &'static str,
149 migration_plan: Option<MigrationPlanConstructor>,
150 ) -> Self {
151 Self {
152 def,
153 memory_namespace,
154 memory_profile: CanisterMemoryProfile::General,
155 migration_plan,
156 }
157 }
158
159 #[must_use]
160 pub const fn def(&self) -> &Def {
161 &self.def
162 }
163
164 #[must_use]
165 pub const fn memory_namespace(&self) -> &'static str {
166 self.memory_namespace
167 }
168
169 #[must_use]
173 pub const fn with_memory_profile(mut self, profile: CanisterMemoryProfile) -> Self {
174 self.memory_profile = profile;
175 self
176 }
177
178 #[must_use]
180 pub const fn memory_profile(&self) -> CanisterMemoryProfile {
181 self.memory_profile
182 }
183
184 pub fn migration_plan(
190 &self,
191 ) -> Result<Option<icydb_schema::SchemaMigrationPlan>, icydb_schema::SchemaContractError> {
192 self.migration_plan
193 .map(|constructor| constructor())
194 .transpose()
195 }
196
197 #[must_use]
198 pub fn commit_stable_key(&self) -> String {
199 stable_memory_key(self.memory_namespace(), "commit", "control")
200 }
201
202 #[must_use]
203 pub fn integrity_progress_stable_key(&self) -> String {
204 stable_memory_key(self.memory_namespace(), "integrity", "progress")
205 }
206
207 #[must_use]
208 pub fn startup_stable_key(&self) -> String {
209 stable_memory_key(self.memory_namespace(), "startup", "control")
210 }
211}
212
213impl MacroNode for Canister {
214 fn as_any(&self) -> &dyn std::any::Any {
215 self
216 }
217}
218
219impl ValidateNode for Canister {
220 fn validate(&self) -> Result<(), ErrorTree> {
221 let mut errs = ErrorTree::new();
222 validate_stable_key_segment(
223 &mut errs,
224 "canister memory_namespace",
225 self.memory_namespace(),
226 );
227 for key in [
228 self.commit_stable_key(),
229 self.startup_stable_key(),
230 self.integrity_progress_stable_key(),
231 ] {
232 validate_stable_key(&mut errs, "canister control key", &key);
233 }
234
235 let canister_path = self.def().path();
236 {
237 let schema = schema_read();
238 let mut seen = BTreeMap::new();
239 for (path, store) in
240 schema.filter_nodes::<Store>(|store| store.canister() == canister_path)
241 {
242 if let Some(config) = store.journaled_memory_config()
243 && let Some(previous) = seen.insert(config.key(), path)
244 {
245 err!(
246 errs,
247 "duplicate store key `{}` in canister `{}`: {} conflicts with {}",
248 config.key(),
249 canister_path,
250 previous,
251 path
252 );
253 }
254 }
255 }
256 errs.result()
257 }
258}
259
260impl VisitableNode for Canister {
261 fn route_key(&self) -> String {
262 self.def().path()
263 }
264}