Skip to main content

icydb_schema/node/
mod.rs

1//! Schema node graph for validated canister/entity/type definitions.
2//!
3//! This module owns the typed node descriptors used by schema validation,
4//! derive code generation, and visitor traversal.
5
6mod arg;
7mod canister;
8mod def;
9mod entity;
10mod r#enum;
11mod field;
12mod index;
13mod item;
14mod list;
15mod map;
16mod newtype;
17mod primary_key;
18mod record;
19mod sanitizer;
20mod schema;
21mod set;
22mod store;
23mod tuple;
24mod r#type;
25mod validator;
26mod value;
27
28use crate::{
29    prelude::*,
30    visit::{Event, Visitor},
31};
32use std::any::Any;
33use thiserror::Error as ThisError;
34
35pub use arg::*;
36pub use canister::*;
37pub use def::*;
38pub use entity::*;
39pub use r#enum::*;
40pub use field::*;
41pub use index::*;
42pub use item::*;
43pub use list::*;
44pub use map::*;
45pub use newtype::*;
46pub use primary_key::*;
47pub use record::*;
48pub use sanitizer::*;
49pub use schema::*;
50pub use set::*;
51pub use store::*;
52pub use tuple::*;
53pub use r#type::*;
54pub use validator::*;
55pub use value::*;
56
57pub const APP_MEMORY_ID_MIN: u8 = 100;
58pub const APP_MEMORY_ID_MAX: u8 = 254;
59const RESERVED_INTERNAL_MEMORY_ID: u8 = u8::MAX;
60
61///
62/// NodeError
63///
64/// Error raised when schema-node lookup or downcasting crosses an invalid
65/// boundary.
66///
67
68#[derive(Debug, ThisError)]
69pub enum NodeError {
70    #[error("{0} is an incorrect node type")]
71    IncorrectNodeType(String),
72
73    #[error("path not found: {0}")]
74    PathNotFound(String),
75}
76
77///
78/// NODE TRAITS
79///
80
81///
82/// MacroNode
83///
84/// Shared trait implemented by every concrete schema node descriptor.
85/// `as_any` keeps type erasure and downcasting local to the schema-node
86/// boundary instead of leaking it into callers.
87///
88
89pub trait MacroNode: Any {
90    fn as_any(&self) -> &dyn Any;
91}
92
93///
94/// TypeNode
95///
96/// Shared trait for schema nodes that expose one canonical runtime `Type`
97/// descriptor to validators and code generators.
98///
99
100pub trait TypeNode: MacroNode {
101    fn ty(&self) -> &Type;
102}
103
104///
105/// ValidateNode
106///
107/// Trait implemented by schema nodes that validate local invariants against
108/// the surrounding schema graph.
109///
110
111pub trait ValidateNode {
112    fn validate(&self) -> Result<(), ErrorTree> {
113        Ok(())
114    }
115}
116
117///
118/// VisitableNode
119///
120/// Trait implemented by schema nodes that participate in recursive visitor
121/// traversal with canonical route-key ordering.
122///
123
124pub trait VisitableNode: ValidateNode {
125    // Route key contributes one node-local path segment to the visitor path.
126    fn route_key(&self) -> String {
127        String::new()
128    }
129
130    // Drive the enter/children/exit visitor sequence for this node.
131    fn accept<V: Visitor>(&self, visitor: &mut V) {
132        visitor.push(&self.route_key());
133        visitor.visit(self, Event::Enter);
134        self.drive(visitor);
135        visitor.visit(self, Event::Exit);
136        visitor.pop();
137    }
138
139    // Visit child nodes in canonical order.
140    fn drive<V: Visitor>(&self, _: &mut V) {}
141}
142
143// Validate one memory id against the declared canister range.
144pub(crate) fn validate_memory_id_in_range(
145    errs: &mut ErrorTree,
146    label: &str,
147    memory_id: u8,
148    min: u8,
149    max: u8,
150) {
151    if memory_id < min || memory_id > max {
152        err!(errs, "{label} {memory_id} outside of range {min}-{max}");
153    }
154}
155
156// Reject memory id values reserved by stable-structures internals.
157pub(crate) fn validate_memory_id_not_reserved(errs: &mut ErrorTree, label: &str, memory_id: u8) {
158    if memory_id == RESERVED_INTERNAL_MEMORY_ID {
159        err!(
160            errs,
161            "{label} {memory_id} is reserved for stable-structures internals",
162        );
163    }
164}
165
166// Validate one application-owned memory id against Canic's stable ABI range.
167pub(crate) fn validate_app_memory_id(errs: &mut ErrorTree, label: &str, memory_id: u8) {
168    if !(APP_MEMORY_ID_MIN..=APP_MEMORY_ID_MAX).contains(&memory_id) {
169        err!(
170            errs,
171            "{label} {memory_id} outside of application memory range {APP_MEMORY_ID_MIN}-{APP_MEMORY_ID_MAX}",
172        );
173    }
174}
175
176pub(crate) fn validate_stable_key_segment(errs: &mut ErrorTree, label: &str, value: &str) {
177    if !stable_key_segment_is_canonical(value) {
178        err!(
179            errs,
180            "{label} `{value}` must use lowercase ASCII letters, digits, and underscores",
181        );
182    }
183}
184
185pub(crate) fn validate_stable_key(errs: &mut ErrorTree, label: &str, value: &str) {
186    if !stable_key_is_canonical(value) {
187        err!(
188            errs,
189            "{label} `{value}` must be canonical lowercase ASCII, must use dots as separators, must use underscores instead of hyphens, must end in .v1, and must not start with canic.",
190        );
191    }
192}
193
194#[must_use]
195pub fn stable_key_segment_is_canonical(value: &str) -> bool {
196    !value.is_empty()
197        && value
198            .bytes()
199            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
200}
201
202#[must_use]
203pub fn stable_key_is_canonical(value: &str) -> bool {
204    if value.starts_with("canic.") {
205        return false;
206    }
207
208    let mut saw_segment = false;
209    let mut last_segment = "";
210    for segment in value.split('.') {
211        if !stable_key_segment_is_canonical(segment) {
212            return false;
213        }
214        saw_segment = true;
215        last_segment = segment;
216    }
217
218    saw_segment && last_segment == "v1"
219}