Skip to main content

icydb_model/node/
schema.rs

1use crate::{Error, prelude::*};
2use icydb_schema::{ScalarLiteral, SchemaContractError, TypeSourceKey};
3use std::{any::Any, collections::BTreeMap};
4
5#[cfg(not(target_arch = "wasm32"))]
6use sha2::{Digest, Sha256};
7
8///
9/// SchemaNode
10///
11
12#[remain::sorted]
13#[derive(Clone, Debug, Serialize)]
14pub enum SchemaNode {
15    Canister(Canister),
16    Entity(Entity),
17    Enum(Enum),
18    List(List),
19    Map(Map),
20    Newtype(Newtype),
21    Normalizer(Normalizer),
22    Record(Record),
23    Set(Set),
24    Store(Store),
25    Tuple(Tuple),
26    Validator(Validator),
27}
28
29impl SchemaNode {
30    const fn def(&self) -> &Def {
31        match self {
32            Self::Canister(n) => n.def(),
33            Self::Entity(n) => n.def(),
34            Self::Enum(n) => n.def(),
35            Self::List(n) => n.def(),
36            Self::Map(n) => n.def(),
37            Self::Newtype(n) => n.def(),
38            Self::Normalizer(n) => n.def(),
39            Self::Record(n) => n.def(),
40            Self::Set(n) => n.def(),
41            Self::Store(n) => n.def(),
42            Self::Tuple(n) => n.def(),
43            Self::Validator(n) => n.def(),
44        }
45    }
46}
47
48impl MacroNode for SchemaNode {
49    fn as_any(&self) -> &dyn Any {
50        match self {
51            Self::Canister(n) => n.as_any(),
52            Self::Entity(n) => n.as_any(),
53            Self::Enum(n) => n.as_any(),
54            Self::List(n) => n.as_any(),
55            Self::Map(n) => n.as_any(),
56            Self::Newtype(n) => n.as_any(),
57            Self::Normalizer(n) => n.as_any(),
58            Self::Record(n) => n.as_any(),
59            Self::Set(n) => n.as_any(),
60            Self::Store(n) => n.as_any(),
61            Self::Tuple(n) => n.as_any(),
62            Self::Validator(n) => n.as_any(),
63        }
64    }
65}
66
67impl ValidateNode for SchemaNode {}
68
69impl VisitableNode for SchemaNode {
70    fn drive<V: Visitor>(&self, v: &mut V) {
71        match self {
72            Self::Canister(n) => n.accept(v),
73            Self::Entity(n) => n.accept(v),
74            Self::Enum(n) => n.accept(v),
75            Self::List(n) => n.accept(v),
76            Self::Map(n) => n.accept(v),
77            Self::Newtype(n) => n.accept(v),
78            Self::Normalizer(n) => n.accept(v),
79            Self::Record(n) => n.accept(v),
80            Self::Set(n) => n.accept(v),
81            Self::Store(n) => n.accept(v),
82            Self::Tuple(n) => n.accept(v),
83            Self::Validator(n) => n.accept(v),
84        }
85    }
86}
87
88///
89/// Schema
90///
91
92#[derive(Clone, Debug, Serialize)]
93pub struct Schema {
94    nodes: BTreeMap<String, SchemaNode>,
95    #[serde(skip)]
96    state: SchemaState,
97    #[serde(skip)]
98    registration_error: Option<SchemaGraphError>,
99}
100
101impl Schema {
102    #[must_use]
103    pub const fn new() -> Self {
104        Self {
105            nodes: BTreeMap::new(),
106            state: SchemaState::Collecting,
107            registration_error: None,
108        }
109    }
110
111    /// Register one constructor-produced node while the graph is collecting.
112    ///
113    /// Duplicate and late registration are retained as graph errors rather
114    /// than replacing an earlier declaration. [`crate::build::get_schema`]
115    /// reports the first retained error before exposing a sealed snapshot.
116    pub fn insert_node(&mut self, node: SchemaNode) {
117        let path = node.def().path();
118        if self.state.is_sealed() {
119            self.record_registration_error(SchemaGraphError::LateRegistration(path));
120            return;
121        }
122        if self.nodes.contains_key(path.as_str()) {
123            self.record_registration_error(SchemaGraphError::DuplicateRegistration(path));
124            return;
125        }
126        self.nodes.insert(path, node);
127    }
128
129    /// Seal the complete graph after whole-graph validation.
130    ///
131    /// # Errors
132    ///
133    /// Returns the first duplicate or late-registration failure retained while
134    /// constructors populated the graph.
135    #[cfg(not(target_arch = "wasm32"))]
136    pub(crate) fn seal(&mut self) -> Result<SchemaGraphDigest, SchemaGraphError> {
137        if let Some(error) = self.registration_error.clone() {
138            return Err(error);
139        }
140        if let SchemaState::Sealed(digest) = self.state {
141            return Ok(digest);
142        }
143
144        let mut hasher = Sha256::new();
145        for (path, node) in &self.nodes {
146            hash_bounded_bytes(&mut hasher, path.as_bytes());
147            let encoded =
148                serde_json::to_vec(node).map_err(|_| SchemaGraphError::SnapshotEncoding)?;
149            hash_bounded_bytes(&mut hasher, encoded.as_slice());
150        }
151        let digest = SchemaGraphDigest(hasher.finalize().into());
152        self.state = SchemaState::Sealed(digest);
153
154        Ok(digest)
155    }
156
157    /// Return whether whole-graph validation has sealed this graph.
158    #[must_use]
159    pub const fn is_sealed(&self) -> bool {
160        self.state.is_sealed()
161    }
162
163    /// Return the immutable digest of a sealed graph.
164    #[must_use]
165    pub const fn digest(&self) -> Option<SchemaGraphDigest> {
166        #[cfg(not(target_arch = "wasm32"))]
167        match self.state {
168            SchemaState::Collecting => None,
169            SchemaState::Sealed(digest) => Some(digest),
170        }
171        #[cfg(target_arch = "wasm32")]
172        {
173            None
174        }
175    }
176
177    fn record_registration_error(&mut self, error: SchemaGraphError) {
178        if self.registration_error.is_none() {
179            self.registration_error = Some(error);
180        }
181    }
182
183    // get_node
184    #[must_use]
185    pub fn get_node<'a>(&'a self, path: &str) -> Option<&'a SchemaNode> {
186        self.nodes.get(path)
187    }
188
189    // try_get_node
190    pub fn try_get_node<'a>(&'a self, path: &str) -> Result<&'a SchemaNode, Error> {
191        let node = self
192            .get_node(path)
193            .ok_or_else(|| NodeError::PathNotFound(path.to_string()))?;
194
195        Ok(node)
196    }
197
198    // cast_node
199    pub fn cast_node<'a, T: 'static>(&'a self, path: &str) -> Result<&'a T, Error> {
200        let node = self.try_get_node(path)?;
201
202        node.as_any()
203            .downcast_ref::<T>()
204            .ok_or_else(|| NodeError::IncorrectNodeType(path.to_string()).into())
205    }
206
207    // check_node_as
208    pub(crate) fn check_node_as<T: 'static>(&self, path: &str) -> Result<(), Error> {
209        self.cast_node::<T>(path).map(|_| ())
210    }
211
212    // get_nodes
213    pub fn get_nodes<T: 'static>(&self) -> impl Iterator<Item = (&str, &T)> {
214        self.nodes
215            .iter()
216            .filter_map(|(key, node)| node.as_any().downcast_ref::<T>().map(|n| (key.as_str(), n)))
217    }
218
219    // filter_nodes
220    // Generic method to filter key, and nodes of any type with a predicate
221    pub fn filter_nodes<'a, T: 'static>(
222        &'a self,
223        predicate: impl Fn(&T) -> bool + 'a,
224    ) -> impl Iterator<Item = (&'a str, &'a T)> + 'a {
225        self.nodes.iter().filter_map(move |(key, node)| {
226            node.as_any()
227                .downcast_ref::<T>()
228                .filter(|target| predicate(target))
229                .map(|target| (key.as_str(), target))
230        })
231    }
232
233    /// Borrow all schema nodes indexed by path.
234    #[must_use]
235    pub const fn nodes(&self) -> &BTreeMap<String, SchemaNode> {
236        &self.nodes
237    }
238
239    /// Resolve one authored unit-enum literal through immutable source keys.
240    ///
241    /// # Errors
242    ///
243    /// Returns an invalid-enum-literal error when the path is not an enum, the
244    /// variant is absent, or either maintained source key is malformed.
245    pub fn enum_unit_literal(
246        &self,
247        enum_path: &str,
248        variant_name: &str,
249    ) -> Result<ScalarLiteral, SchemaContractError> {
250        let r#enum = self
251            .get_node(enum_path)
252            .and_then(|node| node.as_any().downcast_ref::<Enum>())
253            .ok_or(SchemaContractError::InvalidEnumLiteral)?;
254        let variant = r#enum
255            .variants()
256            .iter()
257            .find(|variant| variant.ident() == variant_name && variant.value().is_none())
258            .ok_or(SchemaContractError::InvalidEnumLiteral)?;
259        Ok(ScalarLiteral::EnumUnit {
260            enum_type: TypeSourceKey::try_new(r#enum.source_key())?,
261            variant: TypeSourceKey::try_new(variant.source_key())?,
262        })
263    }
264}
265
266impl Default for Schema {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272impl ValidateNode for Schema {}
273
274impl VisitableNode for Schema {
275    fn drive<V: Visitor>(&self, v: &mut V) {
276        for node in self.nodes.values() {
277            node.accept(v);
278        }
279    }
280}
281
282#[cfg(not(target_arch = "wasm32"))]
283fn hash_bounded_bytes(hasher: &mut Sha256, bytes: &[u8]) {
284    hasher.update((bytes.len() as u64).to_be_bytes());
285    hasher.update(bytes);
286}
287
288///
289/// SchemaGraphDigest
290///
291/// Deterministic identity of one validated, sealed host authoring graph.
292///
293
294#[derive(Clone, Copy, Debug, Eq, PartialEq)]
295pub struct SchemaGraphDigest([u8; 32]);
296
297impl SchemaGraphDigest {
298    /// Return the digest bytes.
299    #[must_use]
300    pub const fn to_bytes(self) -> [u8; 32] {
301        self.0
302    }
303}
304
305///
306/// SchemaState
307///
308/// Construction phase of one host authoring graph.
309///
310
311#[derive(Clone, Copy, Debug, Default)]
312enum SchemaState {
313    #[default]
314    Collecting,
315    #[cfg(not(target_arch = "wasm32"))]
316    Sealed(SchemaGraphDigest),
317}
318
319impl SchemaState {
320    const fn is_sealed(self) -> bool {
321        #[cfg(not(target_arch = "wasm32"))]
322        {
323            matches!(self, Self::Sealed(_))
324        }
325        #[cfg(target_arch = "wasm32")]
326        {
327            false
328        }
329    }
330}
331
332///
333/// SchemaGraphError
334///
335/// Deterministic graph-construction failure retained until sealing.
336///
337
338#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
339pub enum SchemaGraphError {
340    /// A second constructor declared the same complete Rust path.
341    #[error("duplicate authoring-graph registration for '{0}'")]
342    DuplicateRegistration(String),
343
344    /// A constructor attempted to mutate an already sealed graph.
345    #[error("late authoring-graph registration for '{0}'")]
346    LateRegistration(String),
347
348    /// The validated graph could not be encoded for deterministic identity.
349    #[error("authoring graph could not be encoded for deterministic identity")]
350    SnapshotEncoding,
351}
352
353///
354/// TESTS
355///
356
357#[cfg(test)]
358mod tests {
359    use crate::node::{Def, Schema, SchemaGraphError, SchemaNode, Validator};
360
361    fn validator(path: &'static str, ident: &'static str) -> SchemaNode {
362        SchemaNode::Validator(Validator::new(Def::new(path, ident)))
363    }
364
365    #[test]
366    fn sealing_is_deterministic_and_idempotent() {
367        let mut left = Schema::new();
368        left.insert_node(validator("test::beta", "Beta"));
369        left.insert_node(validator("test::alpha", "Alpha"));
370
371        let mut right = Schema::new();
372        right.insert_node(validator("test::alpha", "Alpha"));
373        right.insert_node(validator("test::beta", "Beta"));
374
375        let left_digest = left.seal().expect("left graph should seal");
376        let right_digest = right.seal().expect("right graph should seal");
377
378        assert_eq!(left_digest, right_digest);
379        assert_eq!(
380            left.seal().expect("sealed graph should reuse its digest"),
381            left_digest,
382        );
383    }
384
385    #[test]
386    fn sealing_digest_changes_with_graph_content() {
387        let mut before = Schema::new();
388        before.insert_node(validator("test", "Before"));
389
390        let mut after = Schema::new();
391        after.insert_node(validator("test", "After"));
392
393        assert_ne!(
394            before.seal().expect("before graph should seal"),
395            after.seal().expect("after graph should seal"),
396        );
397    }
398
399    #[test]
400    fn duplicate_registration_fails_without_replacing_the_first_node() {
401        let mut schema = Schema::new();
402        schema.insert_node(validator("test", "Duplicate"));
403        schema.insert_node(validator("test", "Duplicate"));
404
405        assert_eq!(
406            schema.seal(),
407            Err(SchemaGraphError::DuplicateRegistration(
408                "test::Duplicate".to_string(),
409            )),
410        );
411        assert_eq!(schema.nodes().len(), 1);
412    }
413
414    #[test]
415    fn late_registration_fails_without_mutating_the_snapshot() {
416        let mut schema = Schema::new();
417        schema.insert_node(validator("test", "BeforeSeal"));
418        let digest = schema.seal().expect("initial graph should seal");
419
420        schema.insert_node(validator("test", "AfterSeal"));
421
422        assert_eq!(
423            schema.seal(),
424            Err(SchemaGraphError::LateRegistration(
425                "test::AfterSeal".to_string(),
426            )),
427        );
428        assert_eq!(schema.digest(), Some(digest));
429        assert_eq!(schema.nodes().len(), 1);
430    }
431}