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/// Parse one authored textual migration literal into its exact public atom.
20#[doc(hidden)]
21pub fn migration_literal_from_text(
22    kind: &str,
23    value: &str,
24) -> Result<icydb_schema::ScalarLiteral, icydb_schema::SchemaContractError> {
25    use icydb_schema::{
26        Account, Blob, Date, Decimal, Duration, Principal, ScalarLiteral, Subaccount, Timestamp,
27        U256, Ulid,
28    };
29
30    let invalid = || icydb_schema::SchemaContractError::InvalidLiteral;
31    match kind {
32        "account" => Account::from_str(value)
33            .map(ScalarLiteral::Account)
34            .map_err(|_| invalid()),
35        "blob" => Blob::from_hex_literal(value).map(ScalarLiteral::Blob),
36        "date" => Date::parse(value)
37            .map(ScalarLiteral::Date)
38            .ok_or_else(invalid),
39        "decimal" => Decimal::from_str(value)
40            .map(ScalarLiteral::Decimal)
41            .map_err(|_| invalid()),
42        "duration" => Duration::parse_flexible(value)
43            .map(ScalarLiteral::Duration)
44            .map_err(|_| invalid()),
45        "principal" => Principal::from_str(value)
46            .map(ScalarLiteral::Principal)
47            .map_err(|_| invalid()),
48        "subaccount" => {
49            // Migration literals retain their exact unprefixed 32-byte grammar.
50            if value.len() != 64 {
51                return Err(invalid());
52            }
53            Subaccount::from_hex(value).map(ScalarLiteral::Subaccount)
54        }
55        "timestamp" => Timestamp::parse_flexible(value)
56            .map(ScalarLiteral::Timestamp)
57            .map_err(|_| invalid()),
58        "u256" => U256::from_str(value)
59            .map(ScalarLiteral::U256)
60            .map_err(|_| invalid()),
61        "ulid" => Ulid::from_str(value)
62            .map(ScalarLiteral::Ulid)
63            .map_err(|_| invalid()),
64        _ => Err(invalid()),
65    }
66}
67
68///
69/// Canister
70///
71
72#[derive(Clone, Debug, Serialize)]
73pub struct Canister {
74    def: Def,
75    memory_namespace: &'static str,
76    #[serde(skip)]
77    migration_plan: Option<MigrationPlanConstructor>,
78}
79
80impl Canister {
81    #[must_use]
82    pub const fn new(
83        def: Def,
84        memory_namespace: &'static str,
85        migration_plan: Option<MigrationPlanConstructor>,
86    ) -> Self {
87        Self {
88            def,
89            memory_namespace,
90            migration_plan,
91        }
92    }
93
94    #[must_use]
95    pub const fn def(&self) -> &Def {
96        &self.def
97    }
98
99    #[must_use]
100    pub const fn memory_namespace(&self) -> &'static str {
101        self.memory_namespace
102    }
103
104    /// Construct the optional source-declared migration plan.
105    ///
106    /// # Errors
107    ///
108    /// Returns the schema-contract error produced by the bounded declaration.
109    pub fn migration_plan(
110        &self,
111    ) -> Result<Option<icydb_schema::SchemaMigrationPlan>, icydb_schema::SchemaContractError> {
112        self.migration_plan
113            .map(|constructor| constructor())
114            .transpose()
115    }
116
117    #[must_use]
118    pub fn commit_stable_key(&self) -> String {
119        stable_memory_key(self.memory_namespace(), "commit", "control")
120    }
121
122    #[must_use]
123    pub fn integrity_progress_stable_key(&self) -> String {
124        stable_memory_key(self.memory_namespace(), "integrity", "progress")
125    }
126
127    #[must_use]
128    pub fn startup_stable_key(&self) -> String {
129        stable_memory_key(self.memory_namespace(), "startup", "control")
130    }
131}
132
133impl MacroNode for Canister {
134    fn as_any(&self) -> &dyn std::any::Any {
135        self
136    }
137}
138
139impl ValidateNode for Canister {
140    fn validate(&self) -> Result<(), ErrorTree> {
141        let mut errs = ErrorTree::new();
142        validate_stable_key_segment(
143            &mut errs,
144            "canister memory_namespace",
145            self.memory_namespace(),
146        );
147        for key in [
148            self.commit_stable_key(),
149            self.startup_stable_key(),
150            self.integrity_progress_stable_key(),
151        ] {
152            validate_stable_key(&mut errs, "canister control key", &key);
153        }
154
155        let canister_path = self.def().path();
156        {
157            let schema = schema_read();
158            let mut seen = BTreeMap::new();
159            for (path, store) in
160                schema.filter_nodes::<Store>(|store| store.canister() == canister_path)
161            {
162                if let Some(config) = store.journaled_memory_config()
163                    && let Some(previous) = seen.insert(config.key(), path)
164                {
165                    err!(
166                        errs,
167                        "duplicate store key `{}` in canister `{}`: {} conflicts with {}",
168                        config.key(),
169                        canister_path,
170                        previous,
171                        path
172                    );
173                }
174            }
175        }
176        errs.result()
177    }
178}
179
180impl VisitableNode for Canister {
181    fn route_key(&self) -> String {
182        self.def().path()
183    }
184}