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#[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#[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 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 #[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 #[must_use]
160 pub const fn is_sealed(&self) -> bool {
161 self.state.is_sealed()
162 }
163
164 #[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 #[must_use]
187 pub fn get_node<'a>(&'a self, path: &str) -> Option<&'a SchemaNode> {
188 self.nodes.get(path)
189 }
190
191 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 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 pub(crate) fn check_node_as<T: 'static>(&self, path: &str) -> Result<(), Error> {
211 self.cast_node::<T>(path).map(|_| ())
212 }
213
214 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 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 #[must_use]
237 pub const fn nodes(&self) -> &BTreeMap<String, SchemaNode> {
238 &self.nodes
239 }
240
241 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub struct SchemaGraphDigest([u8; 32]);
298
299impl SchemaGraphDigest {
300 #[must_use]
302 pub const fn to_bytes(self) -> [u8; 32] {
303 self.0
304 }
305}
306
307#[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#[derive(Clone, Debug, Eq, thiserror::Error, Ord, PartialEq, PartialOrd)]
341pub enum SchemaGraphError {
342 #[error("duplicate authoring-graph registration for '{0}'")]
344 DuplicateRegistration(String),
345
346 #[error("late authoring-graph registration for '{0}'")]
348 LateRegistration(String),
349
350 #[error("authoring graph could not be encoded for deterministic identity")]
352 SnapshotEncoding,
353}
354
355#[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}