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) {
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 #[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 #[must_use]
159 pub const fn is_sealed(&self) -> bool {
160 self.state.is_sealed()
161 }
162
163 #[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 #[must_use]
185 pub fn get_node<'a>(&'a self, path: &str) -> Option<&'a SchemaNode> {
186 self.nodes.get(path)
187 }
188
189 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 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 pub(crate) fn check_node_as<T: 'static>(&self, path: &str) -> Result<(), Error> {
209 self.cast_node::<T>(path).map(|_| ())
210 }
211
212 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 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 #[must_use]
235 pub const fn nodes(&self) -> &BTreeMap<String, SchemaNode> {
236 &self.nodes
237 }
238
239 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
295pub struct SchemaGraphDigest([u8; 32]);
296
297impl SchemaGraphDigest {
298 #[must_use]
300 pub const fn to_bytes(self) -> [u8; 32] {
301 self.0
302 }
303}
304
305#[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#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
339pub enum SchemaGraphError {
340 #[error("duplicate authoring-graph registration for '{0}'")]
342 DuplicateRegistration(String),
343
344 #[error("late authoring-graph registration for '{0}'")]
346 LateRegistration(String),
347
348 #[error("authoring graph could not be encoded for deterministic identity")]
350 SnapshotEncoding,
351}
352
353#[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}