1mod 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#[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
77pub trait MacroNode: Any {
90 fn as_any(&self) -> &dyn Any;
91}
92
93pub trait TypeNode: MacroNode {
101 fn ty(&self) -> &Type;
102}
103
104pub trait ValidateNode {
112 fn validate(&self) -> Result<(), ErrorTree> {
113 Ok(())
114 }
115}
116
117pub trait VisitableNode: ValidateNode {
125 fn route_key(&self) -> String {
127 String::new()
128 }
129
130 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 fn drive<V: Visitor>(&self, _: &mut V) {}
141}
142
143pub(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
156pub(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
166pub(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}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn app_memory_id_policy_accepts_only_application_range() {
227 for memory_id in APP_MEMORY_ID_MIN..=APP_MEMORY_ID_MAX {
228 let mut errors = ErrorTree::new();
229 validate_app_memory_id(&mut errors, "memory_id", memory_id);
230 validate_memory_id_not_reserved(&mut errors, "memory_id", memory_id);
231 assert!(
232 errors.is_empty(),
233 "schema should accept app memory id {memory_id}: {errors}",
234 );
235 }
236
237 for memory_id in [0, APP_MEMORY_ID_MIN - 1] {
238 let mut errors = ErrorTree::new();
239 validate_app_memory_id(&mut errors, "memory_id", memory_id);
240 assert!(
241 !errors.is_empty(),
242 "schema should reject below-range app memory id {memory_id}",
243 );
244 }
245
246 let mut errors = ErrorTree::new();
247 validate_app_memory_id(&mut errors, "memory_id", u8::MAX);
248 validate_memory_id_not_reserved(&mut errors, "memory_id", u8::MAX);
249 let rendered = errors.to_string();
250 assert!(
251 rendered.contains("outside of application memory range 100-254"),
252 "reserved id should also fail the app range check: {rendered}",
253 );
254 assert!(
255 rendered.contains("reserved for stable-structures internals"),
256 "reserved id should fail closed explicitly: {rendered}",
257 );
258 }
259
260 #[test]
261 fn stable_key_segment_policy_is_canonical_ascii_only() {
262 for segment in ["db", "demo_rpg", "store_1", "v1"] {
263 assert!(stable_key_segment_is_canonical(segment));
264 }
265
266 for segment in ["", "Demo", "demo-rpg", "demo.rpg", "canic.owned"] {
267 assert!(!stable_key_segment_is_canonical(segment));
268 }
269 }
270
271 #[test]
272 fn full_stable_key_policy_rejects_reserved_and_malformed_keys() {
273 assert!(stable_key_is_canonical("icydb.demo_rpg.characters.data.v1"));
274
275 for key in [
276 "canic.demo_rpg.characters.data.v1",
277 "icydb.demo_rpg.characters.data",
278 "icydb.demo-rpg.characters.data.v1",
279 "icydb.demo_rpg..data.v1",
280 "icydb.Demo.characters.data.v1",
281 "icydb.demo_rpg.characters.data.v2",
282 ] {
283 assert!(!stable_key_is_canonical(key), "key should fail: {key}");
284 }
285 }
286}