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 relation;
20mod sanitizer;
21mod schema;
22mod set;
23mod store;
24mod tuple;
25mod r#type;
26mod validator;
27mod value;
28
29use crate::{
30 prelude::*,
31 visit::{Event, Visitor},
32};
33use std::any::Any;
34use thiserror::Error as ThisError;
35
36pub use arg::*;
37pub use canister::*;
38pub use def::*;
39pub use entity::*;
40pub use r#enum::*;
41pub use field::*;
42pub use index::*;
43pub use item::*;
44pub use list::*;
45pub use map::*;
46pub use newtype::*;
47pub use primary_key::*;
48pub use record::*;
49pub use relation::*;
50pub use sanitizer::*;
51pub use schema::*;
52pub use set::*;
53pub use store::*;
54pub use tuple::*;
55pub use r#type::*;
56pub use validator::*;
57pub use value::*;
58
59pub const APP_MEMORY_ID_MIN: u8 = 100;
60pub const APP_MEMORY_ID_MAX: u8 = 254;
61const RESERVED_INTERNAL_MEMORY_ID: u8 = u8::MAX;
62
63#[derive(Debug, ThisError)]
71pub enum NodeError {
72 #[error("{0} is an incorrect node type")]
73 IncorrectNodeType(String),
74
75 #[error("path not found: {0}")]
76 PathNotFound(String),
77}
78
79pub(crate) trait MacroNode: Any {
92 fn as_any(&self) -> &dyn Any;
93}
94
95pub(crate) trait ValidateNode {
103 fn validate(&self) -> Result<(), ErrorTree> {
104 Ok(())
105 }
106}
107
108pub(crate) trait VisitableNode: ValidateNode {
116 fn route_key(&self) -> String {
118 String::new()
119 }
120
121 fn accept<V: Visitor>(&self, visitor: &mut V) {
123 visitor.push(&self.route_key());
124 visitor.visit(self, Event::Enter);
125 self.drive(visitor);
126 visitor.visit(self, Event::Exit);
127 visitor.pop();
128 }
129
130 fn drive<V: Visitor>(&self, _: &mut V) {}
132}
133
134pub(crate) fn validate_memory_id_in_range(
136 errs: &mut ErrorTree,
137 label: &str,
138 memory_id: u8,
139 min: u8,
140 max: u8,
141) {
142 if !memory_id_is_in_range(memory_id, min, max) {
143 err!(errs, "{label} {memory_id} outside of range {min}-{max}");
144 }
145}
146
147pub(crate) fn validate_memory_id_not_reserved(errs: &mut ErrorTree, label: &str, memory_id: u8) {
149 if memory_id_is_reserved(memory_id) {
150 err!(
151 errs,
152 "{label} {memory_id} is reserved for stable-structures internals",
153 );
154 }
155}
156
157pub(crate) fn validate_app_memory_id(errs: &mut ErrorTree, label: &str, memory_id: u8) {
159 if !app_memory_id_is_valid(memory_id) {
160 err!(
161 errs,
162 "{label} {memory_id} outside of application memory range {APP_MEMORY_ID_MIN}-{APP_MEMORY_ID_MAX}",
163 );
164 }
165}
166
167#[must_use]
168pub const fn memory_id_is_in_range(memory_id: u8, min: u8, max: u8) -> bool {
169 memory_id >= min && memory_id <= max
170}
171
172#[must_use]
173pub const fn memory_id_is_reserved(memory_id: u8) -> bool {
174 memory_id == RESERVED_INTERNAL_MEMORY_ID
175}
176
177#[must_use]
178pub const fn app_memory_id_is_valid(memory_id: u8) -> bool {
179 memory_id >= APP_MEMORY_ID_MIN && memory_id <= APP_MEMORY_ID_MAX
180}
181
182pub(crate) fn validate_stable_key_segment(errs: &mut ErrorTree, label: &str, value: &str) {
183 if !stable_key_segment_is_canonical(value) {
184 err!(
185 errs,
186 "{label} `{value}` must use lowercase ASCII letters, digits, and underscores",
187 );
188 }
189}
190
191pub(crate) fn validate_stable_key(errs: &mut ErrorTree, label: &str, value: &str) {
192 if !stable_key_is_canonical(value) {
193 err!(
194 errs,
195 "{label} `{value}` must be at most 128 bytes, must use lowercase ASCII segments beginning with a letter, must use dots as separators, must use underscores instead of hyphens, must end in .v1, and must not start with canic.",
196 );
197 }
198}
199
200#[must_use]
201pub fn stable_key_segment_is_canonical(value: &str) -> bool {
202 let mut bytes = value.bytes();
203 bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
204 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
205}
206
207#[must_use]
208pub(crate) fn stable_key_is_canonical(value: &str) -> bool {
209 if value.len() > 128 || value.starts_with("canic.") {
210 return false;
211 }
212
213 let mut saw_segment = false;
214 let mut last_segment = "";
215 for segment in value.split('.') {
216 if !stable_key_segment_is_canonical(segment) {
217 return false;
218 }
219 saw_segment = true;
220 last_segment = segment;
221 }
222
223 saw_segment && last_segment == "v1"
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn app_memory_id_policy_accepts_only_application_range() {
232 for memory_id in APP_MEMORY_ID_MIN..=APP_MEMORY_ID_MAX {
233 let mut errors = ErrorTree::new();
234 validate_app_memory_id(&mut errors, "memory_id", memory_id);
235 validate_memory_id_not_reserved(&mut errors, "memory_id", memory_id);
236 assert!(
237 errors.is_empty(),
238 "schema should accept app memory id {memory_id}: {errors}",
239 );
240 }
241
242 for memory_id in [0, APP_MEMORY_ID_MIN - 1] {
243 let mut errors = ErrorTree::new();
244 validate_app_memory_id(&mut errors, "memory_id", memory_id);
245 assert!(
246 !errors.is_empty(),
247 "schema should reject below-range app memory id {memory_id}",
248 );
249 }
250
251 let mut errors = ErrorTree::new();
252 validate_app_memory_id(&mut errors, "memory_id", u8::MAX);
253 validate_memory_id_not_reserved(&mut errors, "memory_id", u8::MAX);
254 let rendered = errors.to_string();
255 assert!(
256 rendered.contains("outside of application memory range 100-254"),
257 "reserved id should also fail the app range check: {rendered}",
258 );
259 assert!(
260 rendered.contains("reserved for stable-structures internals"),
261 "reserved id should fail closed explicitly: {rendered}",
262 );
263 }
264
265 #[test]
266 fn stable_key_segment_policy_requires_a_lowercase_letter_prefix() {
267 for segment in ["db", "demo_rpg", "store_1", "v1"] {
268 assert!(stable_key_segment_is_canonical(segment));
269 }
270
271 for segment in [
272 "",
273 "1db",
274 "_db",
275 "Demo",
276 "demo-rpg",
277 "demo.rpg",
278 "canic.owned",
279 ] {
280 assert!(!stable_key_segment_is_canonical(segment));
281 }
282 }
283
284 #[test]
285 fn full_stable_key_policy_rejects_reserved_and_malformed_keys() {
286 assert!(stable_key_is_canonical("icydb.demo_rpg.characters.data.v1"));
287
288 for key in [
289 "canic.demo_rpg.characters.data.v1",
290 "icydb.demo_rpg.characters.data",
291 "icydb.demo-rpg.characters.data.v1",
292 "icydb.demo_rpg..data.v1",
293 "icydb.Demo.characters.data.v1",
294 "icydb.demo_rpg.characters.data.v2",
295 "icydb.1demo.characters.data.v1",
296 "icydb._demo.characters.data.v1",
297 ] {
298 assert!(!stable_key_is_canonical(key), "key should fail: {key}");
299 }
300
301 let maximum = format!("icydb.{}.data.v1", "a".repeat(114));
302 assert_eq!(maximum.len(), 128);
303 assert!(stable_key_is_canonical(&maximum));
304
305 let oversized = format!("icydb.{}.data.v1", "a".repeat(115));
306 assert_eq!(oversized.len(), 129);
307 assert!(!stable_key_is_canonical(&oversized));
308 }
309}