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 lexically canonical retained error before exposing a sealed
116    /// snapshot.
117    pub fn insert_node(&mut self, node: SchemaNode) {
118        let path = node.def().path();
119        if self.state.is_sealed() {
120            self.record_registration_error(SchemaGraphError::LateRegistration(path));
121            return;
122        }
123        if self.nodes.contains_key(path.as_str()) {
124            self.record_registration_error(SchemaGraphError::DuplicateRegistration(path));
125            return;
126        }
127        self.nodes.insert(path, node);
128    }
129
130    /// Seal the complete graph after whole-graph validation.
131    ///
132    /// # Errors
133    ///
134    /// Returns the canonical duplicate or late-registration failure retained
135    /// while constructors populated the graph.
136    #[cfg(not(target_arch = "wasm32"))]
137    pub(crate) fn seal(&mut self) -> Result<SchemaGraphDigest, SchemaGraphError> {
138        if let Some(error) = self.registration_error.clone() {
139            return Err(error);
140        }
141        if let SchemaState::Sealed(digest) = self.state {
142            return Ok(digest);
143        }
144
145        let mut hasher = Sha256::new();
146        for (path, node) in &self.nodes {
147            hash_bounded_bytes(&mut hasher, path.as_bytes());
148            let encoded =
149                serde_json::to_vec(node).map_err(|_| SchemaGraphError::SnapshotEncoding)?;
150            hash_bounded_bytes(&mut hasher, encoded.as_slice());
151        }
152        let digest = SchemaGraphDigest(hasher.finalize().into());
153        self.state = SchemaState::Sealed(digest);
154
155        Ok(digest)
156    }
157
158    /// Return whether whole-graph validation has sealed this graph.
159    #[must_use]
160    pub const fn is_sealed(&self) -> bool {
161        self.state.is_sealed()
162    }
163
164    /// Return the immutable digest of a sealed graph.
165    #[must_use]
166    pub const fn digest(&self) -> Option<SchemaGraphDigest> {
167        #[cfg(not(target_arch = "wasm32"))]
168        match self.state {
169            SchemaState::Collecting => None,
170            SchemaState::Sealed(digest) => Some(digest),
171        }
172        #[cfg(target_arch = "wasm32")]
173        {
174            None
175        }
176    }
177
178    fn record_registration_error(&mut self, error: SchemaGraphError) {
179        match self.registration_error.as_ref() {
180            Some(current) if current <= &error => {}
181            Some(_) | None => self.registration_error = Some(error),
182        }
183    }
184
185    // get_node
186    #[must_use]
187    pub fn get_node<'a>(&'a self, path: &str) -> Option<&'a SchemaNode> {
188        self.nodes.get(path)
189    }
190
191    // try_get_node
192    pub fn try_get_node<'a>(&'a self, path: &str) -> Result<&'a SchemaNode, Error> {
193        let node = self
194            .get_node(path)
195            .ok_or_else(|| NodeError::PathNotFound(path.to_string()))?;
196
197        Ok(node)
198    }
199
200    // cast_node
201    pub fn cast_node<'a, T: 'static>(&'a self, path: &str) -> Result<&'a T, Error> {
202        let node = self.try_get_node(path)?;
203
204        node.as_any()
205            .downcast_ref::<T>()
206            .ok_or_else(|| NodeError::IncorrectNodeType(path.to_string()).into())
207    }
208
209    // check_node_as
210    pub(crate) fn check_node_as<T: 'static>(&self, path: &str) -> Result<(), Error> {
211        self.cast_node::<T>(path).map(|_| ())
212    }
213
214    // get_nodes
215    pub fn get_nodes<T: 'static>(&self) -> impl Iterator<Item = (&str, &T)> {
216        self.nodes
217            .iter()
218            .filter_map(|(key, node)| node.as_any().downcast_ref::<T>().map(|n| (key.as_str(), n)))
219    }
220
221    // filter_nodes
222    // Generic method to filter key, and nodes of any type with a predicate
223    pub fn filter_nodes<'a, T: 'static>(
224        &'a self,
225        predicate: impl Fn(&T) -> bool + 'a,
226    ) -> impl Iterator<Item = (&'a str, &'a T)> + 'a {
227        self.nodes.iter().filter_map(move |(key, node)| {
228            node.as_any()
229                .downcast_ref::<T>()
230                .filter(|target| predicate(target))
231                .map(|target| (key.as_str(), target))
232        })
233    }
234
235    /// Borrow all schema nodes indexed by path.
236    #[must_use]
237    pub const fn nodes(&self) -> &BTreeMap<String, SchemaNode> {
238        &self.nodes
239    }
240
241    /// Resolve one authored unit-enum literal through current declared names.
242    ///
243    /// # Errors
244    ///
245    /// Returns an invalid-enum-literal error when the path is not an enum, the
246    /// variant is absent, or either maintained name is malformed.
247    pub fn enum_unit_literal(
248        &self,
249        enum_path: &str,
250        variant_name: &str,
251    ) -> Result<ScalarLiteral, SchemaContractError> {
252        let r#enum = self
253            .get_node(enum_path)
254            .and_then(|node| node.as_any().downcast_ref::<Enum>())
255            .ok_or(SchemaContractError::InvalidEnumLiteral)?;
256        let variant = r#enum
257            .variants()
258            .iter()
259            .find(|variant| variant.name() == variant_name && variant.value().is_none())
260            .ok_or(SchemaContractError::InvalidEnumLiteral)?;
261        Ok(ScalarLiteral::EnumUnit {
262            enum_type: TypeSourceKey::try_new(r#enum.name())?,
263            variant: TypeSourceKey::try_new(variant.name())?,
264        })
265    }
266}
267
268impl Default for Schema {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl ValidateNode for Schema {}
275
276impl VisitableNode for Schema {
277    fn drive<V: Visitor>(&self, v: &mut V) {
278        for node in self.nodes.values() {
279            node.accept(v);
280        }
281    }
282}
283
284#[cfg(not(target_arch = "wasm32"))]
285fn hash_bounded_bytes(hasher: &mut Sha256, bytes: &[u8]) {
286    hasher.update((bytes.len() as u64).to_be_bytes());
287    hasher.update(bytes);
288}
289
290///
291/// SchemaGraphDigest
292///
293/// Deterministic identity of one validated, sealed host authoring graph.
294///
295
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub struct SchemaGraphDigest([u8; 32]);
298
299impl SchemaGraphDigest {
300    /// Return the digest bytes.
301    #[must_use]
302    pub const fn to_bytes(self) -> [u8; 32] {
303        self.0
304    }
305}
306
307///
308/// SchemaState
309///
310/// Construction phase of one host authoring graph.
311///
312
313#[derive(Clone, Copy, Debug, Default)]
314enum SchemaState {
315    #[default]
316    Collecting,
317    #[cfg(not(target_arch = "wasm32"))]
318    Sealed(SchemaGraphDigest),
319}
320
321impl SchemaState {
322    const fn is_sealed(self) -> bool {
323        #[cfg(not(target_arch = "wasm32"))]
324        {
325            matches!(self, Self::Sealed(_))
326        }
327        #[cfg(target_arch = "wasm32")]
328        {
329            false
330        }
331    }
332}
333
334///
335/// SchemaGraphError
336///
337/// Deterministic graph-construction failure retained until sealing.
338///
339
340#[derive(Clone, Debug, Eq, thiserror::Error, Ord, PartialEq, PartialOrd)]
341pub enum SchemaGraphError {
342    /// A second constructor declared the same complete Rust path.
343    #[error("duplicate authoring-graph registration for '{0}'")]
344    DuplicateRegistration(String),
345
346    /// A constructor attempted to mutate an already sealed graph.
347    #[error("late authoring-graph registration for '{0}'")]
348    LateRegistration(String),
349
350    /// The validated graph could not be encoded for deterministic identity.
351    #[error("authoring graph could not be encoded for deterministic identity")]
352    SnapshotEncoding,
353}
354
355///
356/// TESTS
357///
358
359#[cfg(test)]
360mod tests {
361    use crate::node::{Def, Schema, SchemaGraphError, SchemaNode, Validator};
362
363    fn validator(path: &'static str, ident: &'static str) -> SchemaNode {
364        SchemaNode::Validator(Validator::new(Def::new(path, ident)))
365    }
366
367    #[test]
368    fn sealing_is_deterministic_and_idempotent() {
369        let mut left = Schema::new();
370        left.insert_node(validator("test::beta", "Beta"));
371        left.insert_node(validator("test::alpha", "Alpha"));
372
373        let mut right = Schema::new();
374        right.insert_node(validator("test::alpha", "Alpha"));
375        right.insert_node(validator("test::beta", "Beta"));
376
377        let left_digest = left.seal().expect("left graph should seal");
378        let right_digest = right.seal().expect("right graph should seal");
379
380        assert_eq!(left_digest, right_digest);
381        assert_eq!(
382            left.seal().expect("sealed graph should reuse its digest"),
383            left_digest,
384        );
385    }
386
387    #[test]
388    fn sealing_digest_changes_with_graph_content() {
389        let mut before = Schema::new();
390        before.insert_node(validator("test", "Before"));
391
392        let mut after = Schema::new();
393        after.insert_node(validator("test", "After"));
394
395        assert_ne!(
396            before.seal().expect("before graph should seal"),
397            after.seal().expect("after graph should seal"),
398        );
399    }
400
401    #[test]
402    fn duplicate_registration_fails_without_replacing_the_first_node() {
403        let mut schema = Schema::new();
404        schema.insert_node(validator("test", "Duplicate"));
405        schema.insert_node(validator("test", "Duplicate"));
406
407        assert_eq!(
408            schema.seal(),
409            Err(SchemaGraphError::DuplicateRegistration(
410                "test::Duplicate".to_string(),
411            )),
412        );
413        assert_eq!(schema.nodes().len(), 1);
414    }
415
416    #[test]
417    fn duplicate_registration_diagnostic_is_constructor_order_independent() {
418        let mut left = Schema::new();
419        left.insert_node(validator("test", "Beta"));
420        left.insert_node(validator("test", "Beta"));
421        left.insert_node(validator("test", "Alpha"));
422        left.insert_node(validator("test", "Alpha"));
423
424        let mut right = Schema::new();
425        right.insert_node(validator("test", "Alpha"));
426        right.insert_node(validator("test", "Alpha"));
427        right.insert_node(validator("test", "Beta"));
428        right.insert_node(validator("test", "Beta"));
429
430        let expected = Err(SchemaGraphError::DuplicateRegistration(
431            "test::Alpha".to_string(),
432        ));
433        assert_eq!(left.seal(), expected);
434        assert_eq!(right.seal(), expected);
435    }
436
437    #[test]
438    fn late_registration_fails_without_mutating_the_snapshot() {
439        let mut schema = Schema::new();
440        schema.insert_node(validator("test", "BeforeSeal"));
441        let digest = schema.seal().expect("initial graph should seal");
442
443        schema.insert_node(validator("test", "AfterSeal"));
444
445        assert_eq!(
446            schema.seal(),
447            Err(SchemaGraphError::LateRegistration(
448                "test::AfterSeal".to_string(),
449            )),
450        );
451        assert_eq!(schema.digest(), Some(digest));
452        assert_eq!(schema.nodes().len(), 1);
453    }
454}