Skip to main content

icydb_model/node/
canister.rs

1//! Module: node::canister
2//!
3//! Responsibility: canister-level schema node metadata and memory allocation validation.
4//! Does not own: ICP lifecycle management or runtime stable-memory implementation.
5//! Boundary: validates permanent namespaces and store keys before runtime use.
6
7#[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
15/// Build-time constructor for one source-declared coordinated migration plan.
16pub type MigrationPlanConstructor =
17    fn() -> Result<icydb_schema::SchemaMigrationPlan, icydb_schema::SchemaContractError>;
18
19///
20/// CanisterMemoryProfile
21///
22/// Build-time bucket sizing for IcyDB-owned shared memory bootstrap. This is
23/// physical configuration, not accepted schema authority or allocation access.
24/// A host that bootstraps first owns the effective setting instead.
25///
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
28#[serde(rename_all = "snake_case")]
29pub enum CanisterMemoryProfile {
30    /// 256 KiB buckets; 8 GiB shared manager capacity before backing limits.
31    Compact,
32    /// 1 MiB buckets; 32 GiB shared manager capacity before backing limits.
33    General,
34    /// 8 MiB buckets; 256 GiB shared manager capacity before backing limits.
35    HighHeadroom,
36}
37
38impl CanisterMemoryProfile {
39    /// Return the immutable bucket size in 64 KiB Wasm pages.
40    #[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/// Parse one authored textual migration literal into its exact public atom.
51#[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            // Migration literals retain their exact unprefixed 32-byte grammar.
81            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///
100/// Canister
101///
102
103#[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    /// Select the profile used when IcyDB owns memory bootstrap.
138    ///
139    /// Existing memory must match its bucket size; this does not resize it.
140    #[must_use]
141    pub const fn with_memory_profile(mut self, profile: CanisterMemoryProfile) -> Self {
142        self.memory_profile = profile;
143        self
144    }
145
146    /// Return the configured profile; newly constructed nodes use `General`.
147    #[must_use]
148    pub const fn memory_profile(&self) -> CanisterMemoryProfile {
149        self.memory_profile
150    }
151
152    /// Construct the optional source-declared migration plan.
153    ///
154    /// # Errors
155    ///
156    /// Returns the schema-contract error produced by the bounded declaration.
157    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}