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    #[cfg_attr(
323        target_arch = "wasm32",
324        expect(
325            clippy::unused_self,
326            reason = "Wasm retains only the collecting state while callers share the host state-machine method"
327        )
328    )]
329    const fn is_sealed(self) -> bool {
330        #[cfg(not(target_arch = "wasm32"))]
331        {
332            matches!(self, Self::Sealed(_))
333        }
334        #[cfg(target_arch = "wasm32")]
335        {
336            false
337        }
338    }
339}
340
341///
342/// SchemaGraphError
343///
344/// Deterministic graph-construction failure retained until sealing.
345///
346
347#[derive(Clone, Debug, Eq, thiserror::Error, Ord, PartialEq, PartialOrd)]
348pub enum SchemaGraphError {
349    /// A second constructor declared the same complete Rust path.
350    #[error("duplicate authoring-graph registration for '{0}'")]
351    DuplicateRegistration(String),
352
353    /// A constructor attempted to mutate an already sealed graph.
354    #[error("late authoring-graph registration for '{0}'")]
355    LateRegistration(String),
356
357    /// The validated graph could not be encoded for deterministic identity.
358    #[error("authoring graph could not be encoded for deterministic identity")]
359    SnapshotEncoding,
360}
361
362///
363/// TESTS
364///
365
366#[cfg(test)]
367mod tests {
368    use crate::node::{Def, Schema, SchemaGraphError, SchemaNode, Validator};
369
370    fn validator(path: &'static str, ident: &'static str) -> SchemaNode {
371        SchemaNode::Validator(Validator::new(Def::new(path, ident)))
372    }
373
374    #[test]
375    fn sealing_is_deterministic_and_idempotent() {
376        let mut left = Schema::new();
377        left.insert_node(validator("test::beta", "Beta"));
378        left.insert_node(validator("test::alpha", "Alpha"));
379
380        let mut right = Schema::new();
381        right.insert_node(validator("test::alpha", "Alpha"));
382        right.insert_node(validator("test::beta", "Beta"));
383
384        let left_digest = left.seal().expect("left graph should seal");
385        let right_digest = right.seal().expect("right graph should seal");
386
387        assert_eq!(left_digest, right_digest);
388        assert_eq!(
389            left.seal().expect("sealed graph should reuse its digest"),
390            left_digest,
391        );
392    }
393
394    #[test]
395    fn sealing_digest_changes_with_graph_content() {
396        let mut before = Schema::new();
397        before.insert_node(validator("test", "Before"));
398
399        let mut after = Schema::new();
400        after.insert_node(validator("test", "After"));
401
402        assert_ne!(
403            before.seal().expect("before graph should seal"),
404            after.seal().expect("after graph should seal"),
405        );
406    }
407
408    #[test]
409    fn duplicate_registration_fails_without_replacing_the_first_node() {
410        let mut schema = Schema::new();
411        schema.insert_node(validator("test", "Duplicate"));
412        schema.insert_node(validator("test", "Duplicate"));
413
414        assert_eq!(
415            schema.seal(),
416            Err(SchemaGraphError::DuplicateRegistration(
417                "test::Duplicate".to_string(),
418            )),
419        );
420        assert_eq!(schema.nodes().len(), 1);
421    }
422
423    #[test]
424    fn duplicate_registration_diagnostic_is_constructor_order_independent() {
425        let mut left = Schema::new();
426        left.insert_node(validator("test", "Beta"));
427        left.insert_node(validator("test", "Beta"));
428        left.insert_node(validator("test", "Alpha"));
429        left.insert_node(validator("test", "Alpha"));
430
431        let mut right = Schema::new();
432        right.insert_node(validator("test", "Alpha"));
433        right.insert_node(validator("test", "Alpha"));
434        right.insert_node(validator("test", "Beta"));
435        right.insert_node(validator("test", "Beta"));
436
437        let expected = Err(SchemaGraphError::DuplicateRegistration(
438            "test::Alpha".to_string(),
439        ));
440        assert_eq!(left.seal(), expected);
441        assert_eq!(right.seal(), expected);
442    }
443
444    #[test]
445    fn late_registration_fails_without_mutating_the_snapshot() {
446        let mut schema = Schema::new();
447        schema.insert_node(validator("test", "BeforeSeal"));
448        let digest = schema.seal().expect("initial graph should seal");
449
450        schema.insert_node(validator("test", "AfterSeal"));
451
452        assert_eq!(
453            schema.seal(),
454            Err(SchemaGraphError::LateRegistration(
455                "test::AfterSeal".to_string(),
456            )),
457        );
458        assert_eq!(schema.digest(), Some(digest));
459        assert_eq!(schema.nodes().len(), 1);
460    }
461}