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 canonical lowercase ASCII, 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 !value.is_empty()
203 && value
204 .bytes()
205 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
206}
207
208#[must_use]
209pub(crate) fn stable_key_is_canonical(value: &str) -> bool {
210 if value.starts_with("canic.") {
211 return false;
212 }
213
214 let mut saw_segment = false;
215 let mut last_segment = "";
216 for segment in value.split('.') {
217 if !stable_key_segment_is_canonical(segment) {
218 return false;
219 }
220 saw_segment = true;
221 last_segment = segment;
222 }
223
224 saw_segment && last_segment == "v1"
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn app_memory_id_policy_accepts_only_application_range() {
233 for memory_id in APP_MEMORY_ID_MIN..=APP_MEMORY_ID_MAX {
234 let mut errors = ErrorTree::new();
235 validate_app_memory_id(&mut errors, "memory_id", memory_id);
236 validate_memory_id_not_reserved(&mut errors, "memory_id", memory_id);
237 assert!(
238 errors.is_empty(),
239 "schema should accept app memory id {memory_id}: {errors}",
240 );
241 }
242
243 for memory_id in [0, APP_MEMORY_ID_MIN - 1] {
244 let mut errors = ErrorTree::new();
245 validate_app_memory_id(&mut errors, "memory_id", memory_id);
246 assert!(
247 !errors.is_empty(),
248 "schema should reject below-range app memory id {memory_id}",
249 );
250 }
251
252 let mut errors = ErrorTree::new();
253 validate_app_memory_id(&mut errors, "memory_id", u8::MAX);
254 validate_memory_id_not_reserved(&mut errors, "memory_id", u8::MAX);
255 let rendered = errors.to_string();
256 assert!(
257 rendered.contains("outside of application memory range 100-254"),
258 "reserved id should also fail the app range check: {rendered}",
259 );
260 assert!(
261 rendered.contains("reserved for stable-structures internals"),
262 "reserved id should fail closed explicitly: {rendered}",
263 );
264 }
265
266 #[test]
267 fn stable_key_segment_policy_is_canonical_ascii_only() {
268 for segment in ["db", "demo_rpg", "store_1", "v1"] {
269 assert!(stable_key_segment_is_canonical(segment));
270 }
271
272 for segment in ["", "Demo", "demo-rpg", "demo.rpg", "canic.owned"] {
273 assert!(!stable_key_segment_is_canonical(segment));
274 }
275 }
276
277 #[test]
278 fn full_stable_key_policy_rejects_reserved_and_malformed_keys() {
279 assert!(stable_key_is_canonical("icydb.demo_rpg.characters.data.v1"));
280
281 for key in [
282 "canic.demo_rpg.characters.data.v1",
283 "icydb.demo_rpg.characters.data",
284 "icydb.demo-rpg.characters.data.v1",
285 "icydb.demo_rpg..data.v1",
286 "icydb.Demo.characters.data.v1",
287 "icydb.demo_rpg.characters.data.v2",
288 ] {
289 assert!(!stable_key_is_canonical(key), "key should fail: {key}");
290 }
291 }
292}